canina/frontend/application/components/CheckoutPage.tsx

774 lines
40 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,
Phone,
AlertCircle
} 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 { pets, activePetId, getActivePet, updatePet } = usePetStore();
const { profile, isLoggedIn } = useUserStore();
const [isHydrated, setIsHydrated] = useState(false);
const [loading, setLoading] = useState(false);
useEffect(() => {
setIsHydrated(true);
}, []);
const [useRoundUp, setUseRoundUp] = useState(false);
const [paymentMethod, setPaymentMethod] = useState<string>('card');
const [showTopUpModal, setShowTopUpModal] = useState(false);
const [showAddressModal, setShowAddressModal] = useState(false);
const isRefillEnabled = useSettingsStore((state) => state.getText('REFILL_SUBSCRIPTION_ENABLED', 'false') === 'true');
const refillPercent = Number(useSettingsStore((state) => state.getText('REFILL_REWARD_PERCENT', '5'))) || 5;
const [selectedPetId, setSelectedPetId] = useState<string | null>(activePetId || (pets[0]?.id ?? null));
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 baseShippingFee = Number(shippingFeeSetting) || 0;
const freeThreshold = Number(useSettingsStore((state) => state.getText('free_shipping_threshold', '2000000'))) || 0;
const subtotal = getSubtotal();
const shippingFee = (freeThreshold > 0 && subtotal >= freeThreshold) ? 0 : baseShippingFee;
const isCardToCardEnabled = useSettingsStore((state) =>
state.getBoolean('PAY_GATEWAY_CARD_ENABLE', true) &&
state.getBoolean('cardToCardEnabled', true) &&
state.getBoolean('cart_to_cart_enabled', true) &&
state.getBoolean('payment_card_to_card_enabled', true)
);
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) {
const neededAmount = totalAmount - (profile.walletBalance || 0);
toast.error(
`موجودی کیف پول شما (${(profile.walletBalance || 0).toLocaleString('fa-IR')} تومان) کمتر از مبلغ سفارش (${totalAmount.toLocaleString('fa-IR')} تومان) است. کادر افزایش موجودی (${neededAmount.toLocaleString('fa-IR')} تومان) باز شد.`,
);
setShowTopUpModal(true);
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: selectedPetId || undefined,
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(() => {});
if (!(err as Record<string, boolean>)?._toastShown) {
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;
if (backendMessage && !backendMessage.includes("Request failed")) {
toast.error(`خطا در ثبت سفارش: ${backendMessage}`);
}
}
} finally {
setLoading(false);
}
};
const getText = useSettingsStore(state => state.getText);
const isCatalogMode = getText('catalog_mode', 'false') === 'true';
const isCatalogDisableCheckout = getText('catalog_disable_checkout', 'false') === 'true';
const isCheckoutDisabled = isCatalogMode || isCatalogDisableCheckout;
if (isCheckoutDisabled) {
return (
<div className="min-h-[70vh] bg-medical-gray-50 flex items-center justify-center p-6 text-center font-vazir" dir="rtl">
<div className="max-w-md bg-white border border-medical-gray-200 rounded-3xl p-8 shadow-xl space-y-6">
<div className="w-16 h-16 bg-amber-50 text-amber-600 rounded-2xl flex items-center justify-center mx-auto">
<AlertCircle className="w-8 h-8" />
</div>
<h2 className="text-xl font-black text-medical-gray-900">
امکان ثبت سفارش آنلاین موقتاً غیرفعال است
</h2>
<p className="text-xs sm:text-sm text-medical-gray-600 font-bold leading-relaxed">
سامانه در حال حاضر در حالت کاتالوگ قرار دارد. جهت استعلام قیمت و دریافت مشاوره تخصصی میتوانید با کارشناسان ما تماس بگیرید.
</p>
<div className="flex flex-col sm:flex-row gap-3 pt-2">
<a
href={getText('contact_phone_link', `tel:${getText('contact_phone', '02188884444').replace(/[^0-9+]/g, '')}`)}
className="flex-1 px-4 py-3 bg-canina-blue hover:bg-canina-dark text-white rounded-xl text-xs font-black transition-all shadow-md flex items-center justify-center gap-2"
>
<Phone className="w-4 h-4" />
<span>تماس با پشتیبانی ({getText('contact_phone', '۰۲۱-۸۸۸۸۴۴۴۴')})</span>
</a>
<button
onClick={() => router.push('/shop')}
className="flex-1 px-4 py-3 bg-medical-gray-100 hover:bg-medical-gray-200 text-medical-gray-700 rounded-xl text-xs font-bold transition-all"
>
مشاهده کاتالوگ
</button>
</div>
</div>
</div>
);
}
if (isHydrated && 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/60 py-4 sm:py-8 px-3 sm:px-6 font-vazir w-full max-w-full overflow-x-hidden text-right" dir="rtl">
<div className="max-w-7xl mx-auto w-full">
{/* Navigation & Breadcrumbs */}
<div className="flex items-center justify-between gap-2 mb-5">
<div className="flex items-center gap-2 text-xs font-bold text-medical-gray-500">
<button
onClick={() => router.back()}
className="flex items-center gap-1 hover:text-canina-blue transition-colors cursor-pointer"
>
<ChevronRight className="w-4 h-4" />
<span>بازگشت</span>
</button>
<span className="text-medical-gray-300">/</span>
<span className="cursor-pointer hover:text-canina-blue" onClick={() => router.push('/shop')}>سبد خرید</span>
<span className="text-medical-gray-300">/</span>
<span className="text-canina-blue font-black">تسویهحساب و پرداخت</span>
</div>
<div className="hidden sm:flex items-center gap-1.5 text-xs text-emerald-700 bg-emerald-50 border border-emerald-200 px-3 py-1 rounded-full font-bold">
<ShieldCheck className="w-3.5 h-3.5" />
<span>پرداخت ۱۰۰٪ امن با پروتکل SSL</span>
</div>
</div>
<div className="grid lg:grid-cols-12 gap-5 w-full items-start">
{/* Main Content (8 cols) */}
<div className="lg:col-span-8 space-y-4 w-full min-w-0">
{/* Step 1: Shipping */}
<section className="bg-white rounded-2xl border border-medical-gray-200 p-4 sm:p-6 shadow-xs relative w-full overflow-hidden">
<div className="flex items-center justify-between pb-3 mb-4 border-b border-medical-gray-100">
<div className="flex items-center gap-2.5">
<div className="w-7 h-7 bg-canina-blue text-white rounded-lg flex items-center justify-center font-black text-xs">۱</div>
<h2 className="text-base sm:text-lg font-black text-medical-gray-900">اطلاعات تحویل و گیرنده</h2>
</div>
{isLoggedIn && (
<button
type="button"
onClick={() => setShowAddressModal(true)}
className="flex items-center gap-1 text-canina-blue font-bold text-xs hover:underline bg-canina-blue/5 border border-canina-blue/20 px-2.5 py-1 rounded-lg transition-all cursor-pointer"
>
<PlusCircle className="w-3.5 h-3.5" />
<span>+ افزودن آدرس</span>
</button>
)}
</div>
{isLoggedIn && profile.addresses && profile.addresses.length > 0 ? (
<div className="space-y-3">
<div className="grid sm:grid-cols-2 gap-2.5">
{profile.addresses.map((addr) => (
<div
key={addr.id}
onClick={() => setSelectedAddressId(addr.id)}
className={cn(
"p-3 rounded-xl border transition-all flex items-start gap-2.5 cursor-pointer",
selectedAddressId === addr.id ? "border-canina-blue bg-canina-blue/5 shadow-xs" : "border-medical-gray-200 hover:border-medical-gray-300"
)}
>
<input
type="radio"
name="selectedAddress"
checked={selectedAddressId === addr.id}
onChange={() => setSelectedAddressId(addr.id)}
className="mt-1 shrink-0 cursor-pointer"
/>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-0.5">
<span className="font-black text-xs text-medical-gray-900">{addr.title}</span>
{addr.isDefault && (
<span className="bg-canina-blue/10 text-canina-blue text-[9px] font-bold px-1.5 py-0.2 rounded-md">پیشفرض</span>
)}
</div>
<p className="text-[11px] text-medical-gray-600 line-clamp-2 leading-relaxed">{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="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-3">
<div>
<label htmlFor="chk-fullname" className="text-[11px] font-bold text-medical-gray-700 block mb-1">نام و نام خانوادگی *</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/70 border border-medical-gray-200 rounded-xl py-2 px-3 text-xs focus:ring-1 focus:ring-canina-blue outline-none"
placeholder="مثلاً: پارسا آقایی"
/>
</div>
<div>
<label htmlFor="chk-tel" className="text-[11px] font-bold text-medical-gray-700 block mb-1">شماره همراه *</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/70 border border-medical-gray-200 rounded-xl py-2 px-3 text-xs focus:ring-1 focus:ring-canina-blue outline-none dir-ltr text-right"
placeholder="۰۹۱۲XXXXXXX"
/>
</div>
<div>
<label htmlFor="chk-province" className="text-[11px] font-bold text-medical-gray-700 block mb-1">استان *</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>
<label htmlFor="chk-city" className="text-[11px] font-bold text-medical-gray-700 block mb-1">شهر *</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/70 border border-medical-gray-200 rounded-xl py-2 px-3 text-xs focus:ring-1 focus:ring-canina-blue outline-none"
placeholder="نام شهر..."
/>
)}
</div>
<div className="sm:col-span-2 md:col-span-3">
<label htmlFor="chk-street" className="text-[11px] font-bold text-medical-gray-700 block mb-1">آدرس دقیق پستی *</label>
<input
id="chk-street"
name="street-address"
autoComplete="street-address"
value={manualAddress}
onChange={(e) => setManualAddress(e.target.value)}
className="w-full bg-medical-gray-50/70 border border-medical-gray-200 rounded-xl py-2 px-3 text-xs focus:ring-1 focus:ring-canina-blue outline-none"
placeholder="خیابان، کوچه، پلاک، واحد..."
/>
</div>
<div>
<label htmlFor="chk-zipcode" className="text-[11px] font-bold text-medical-gray-700 block mb-1">کد پستی (۱۰ رقمی)</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/70 border border-medical-gray-200 rounded-xl py-2 px-3 text-xs focus:ring-1 focus:ring-canina-blue outline-none dir-ltr text-right"
placeholder="۱۲۳۴۵۶۷۸۹۰"
/>
</div>
</div>
)}
</section>
{/* Pet Selection (if user has pets) */}
{pets.length > 0 && (
<section className="bg-white rounded-2xl border border-medical-gray-200 p-4 shadow-xs">
<div className="flex items-center gap-2 mb-2.5">
<span className="text-base">🐾</span>
<h3 className="text-xs font-black text-medical-gray-900">اتصال سفارش به پرونده سلامت حیوان خانگی:</h3>
</div>
<div className="flex flex-wrap gap-2">
<button
type="button"
onClick={() => setSelectedPetId(null)}
className={cn(
"px-3 py-1.5 rounded-xl border text-xs font-bold transition-all cursor-pointer",
selectedPetId === null ? "border-canina-blue bg-canina-blue/10 text-canina-blue" : "border-medical-gray-200 text-medical-gray-600 hover:bg-medical-gray-50"
)}
>
خرید عمومی (بدون پت)
</button>
{pets.map(p => (
<button
key={p.id}
type="button"
onClick={() => setSelectedPetId(p.id)}
className={cn(
"px-3 py-1.5 rounded-xl border text-xs font-bold transition-all cursor-pointer",
selectedPetId === p.id ? "border-canina-blue bg-canina-blue/10 text-canina-blue" : "border-medical-gray-200 text-medical-gray-600 hover:bg-medical-gray-50"
)}
>
{p.name} ({p.breed || p.type})
</button>
))}
</div>
</section>
)}
{/* Step 2: Payment Methods */}
<section className="bg-white rounded-2xl border border-medical-gray-200 p-4 sm:p-6 shadow-xs relative">
<div className="flex items-center gap-2.5 pb-3 mb-4 border-b border-medical-gray-100">
<div className="w-7 h-7 bg-canina-blue text-white rounded-lg flex items-center justify-center font-black text-xs">۲</div>
<h2 className="text-base sm:text-lg font-black text-medical-gray-900">شیوه پرداخت</h2>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{(() => {
const getText = useSettingsStore.getState().getText;
const allGateways = [
{
id: 'online',
label: getText('PAY_GATEWAY_ONLINE_LABEL', 'درگاه پرداخت اینترنتی (زیبال / شتاب)'),
desc: 'پرداخت آنلاین و آنی با کلیه کارت‌های شتاب',
icon: <ShieldCheck className="w-5 h-5" />,
enabled: getText('PAY_GATEWAY_ONLINE_ENABLE', 'true') === 'true'
},
{
id: 'wallet',
label: getText('PAY_GATEWAY_WALLET_LABEL', 'اعتبار حساب (کیف پول)'),
desc: `موجودی: ${toPersian((profile.walletBalance || 0).toLocaleString())} تومان`,
icon: <Wallet className="w-5 h-5" />,
enabled: getText('PAY_GATEWAY_WALLET_ENABLE', 'true') === 'true'
},
{
id: 'card',
label: getText('PAY_GATEWAY_CARD_LABEL', 'کارت به کارت (واریز مستقیم)'),
desc: getText('PAY_GATEWAY_CARD_DESC', 'واریز به شماره کارت بانک سامان'),
icon: <CreditCard className="w-5 h-5" />,
enabled: isCardToCardEnabled
},
{
id: 'cod',
label: getText('PAY_GATEWAY_COD_LABEL', 'پرداخت در محل (کارتخوان)'),
desc: getText('PAY_GATEWAY_COD_DESC', 'تحویل کالا و پرداخت درب منزل'),
icon: <Truck className="w-5 h-5" />,
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-3.5 rounded-xl border-2 transition-all flex items-center justify-between gap-3 relative cursor-pointer",
pay.enabled
? (paymentMethod === pay.id ? "border-canina-blue bg-canina-blue/5 shadow-xs" : "border-medical-gray-200 hover:border-canina-blue/40")
: "border-medical-gray-100 bg-medical-gray-50 opacity-60 cursor-not-allowed"
)}
>
<div className="flex items-center gap-3 min-w-0">
<div className={cn(
"w-9 h-9 rounded-xl flex items-center justify-center shrink-0",
paymentMethod === pay.id ? "bg-canina-blue text-white" : "bg-medical-gray-100 text-medical-gray-500"
)}>
{pay.icon}
</div>
<div className="min-w-0">
<h4 className="font-bold text-xs text-medical-gray-900 truncate">{pay.label}</h4>
<p className="text-[10px] text-medical-gray-500 truncate mt-0.5">{pay.desc}</p>
</div>
</div>
{pay.id === 'wallet' && pay.enabled && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setShowTopUpModal(true);
}}
className="text-[10px] bg-canina-blue text-white px-2.5 py-1 rounded-lg font-bold hover:bg-blue-700 transition-colors shrink-0 shadow-xs cursor-pointer"
>
+ افزایش
</button>
)}
</div>
));
})()}
</div>
{isCardToCardEnabled && paymentMethod === 'card' && (
<div className="mt-4 bg-blue-50/80 border border-blue-200 rounded-xl p-3.5 space-y-2">
<div className="flex items-center gap-2 text-canina-blue font-bold text-xs">
<CreditCard className="w-4 h-4" />
<span>اطلاعات کارت جهت واریز وجه سفارش:</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 text-xs bg-white p-3 rounded-lg border border-blue-100">
<div className="flex items-center justify-between p-2 bg-gray-50 rounded-lg">
<div>
<span className="text-[10px] text-gray-500 block">شماره کارت سامان:</span>
<span className="font-mono font-bold text-xs" dir="ltr">6219 8619 7401 2071</span>
</div>
<button
type="button"
onClick={() => handleCopy("6219861974012071", "شماره کارت")}
className="text-[10px] text-canina-blue hover:underline font-bold"
>
کپی
</button>
</div>
<div className="flex items-center justify-between p-2 bg-gray-50 rounded-lg">
<div>
<span className="text-[10px] text-gray-500 block">صاحب حساب:</span>
<span className="font-bold text-xs text-gray-900">پارسادرمانی کنینا (نوواگارد)</span>
</div>
</div>
</div>
</div>
)}
</section>
{/* Charity Donation Banner (Compact) */}
<section className="bg-pink-50/70 border border-pink-200/80 rounded-2xl p-3.5 flex items-center justify-between gap-3">
<div className="flex items-center gap-2.5">
<div className="w-8 h-8 bg-pink-500 text-white rounded-xl flex items-center justify-center shrink-0">
<Heart className="w-4 h-4 fill-white" />
</div>
<div>
<h4 className="text-xs font-bold text-medical-gray-900">کمک به حیوانات پناهگاه (ردپای مهربانی)</h4>
<p className="text-[10px] text-medical-gray-500">رند کردن مبلغ فاکتور تا {toPersian(roundStep.toLocaleString())} تومان بعدی</p>
</div>
</div>
<button
type="button"
onClick={handleCharityToggle}
className={cn(
"px-3 py-1.5 rounded-xl text-xs font-bold transition-all shrink-0 cursor-pointer",
useRoundUp ? "bg-pink-500 text-white shadow-xs" : "bg-white text-pink-600 border border-pink-200 hover:bg-pink-50"
)}
>
{useRoundUp ? `اهدای ${toPersian(charityDonation.toLocaleString())} ت` : 'مشارکت در خیریه'}
</button>
</section>
</div>
{/* Sticky Order Summary Sidebar (4 cols) */}
<div className="lg:col-span-4 w-full">
<div className="bg-medical-gray-900 rounded-2xl p-5 text-white shadow-xl sticky top-24 space-y-4">
<div className="flex items-center justify-between border-b border-white/10 pb-3">
<h3 className="text-base font-black text-white">خلاصه سفارش</h3>
<span className="text-[11px] text-white/60 font-bold">{toPersian(items.length)} قلم کالا</span>
</div>
{/* Items List (Compact) */}
<div className="space-y-2.5 max-h-48 overflow-y-auto pr-1">
{items.map((item) => (
<div key={item.product.id} className="flex justify-between items-center text-xs gap-2">
<div className="min-w-0 flex-1">
<p data-testid="checkout-item-name" className="font-bold text-white/90 truncate">{item.product.name}</p>
<p className="text-[10px] text-white/50">{toPersian(item.quantity)} بسته</p>
</div>
<span className="font-bold font-vazir text-white whitespace-nowrap">
{toPersian((item.product.priceValue * item.quantity).toLocaleString())} ت
</span>
</div>
))}
</div>
{/* Cost Breakdown */}
<div className="space-y-2 pt-3 border-t border-white/10 text-xs">
<div className="flex justify-between text-white/80">
<span>مجموع اقلام</span>
<span className="font-vazir text-white">{toPersian(getSubtotal().toLocaleString())} تومان</span>
</div>
{isRefillEnabled && isSubscribed && (
<div className="flex justify-between text-xs font-bold text-emerald-400 bg-emerald-500/10 p-2 rounded-xl border border-emerald-500/20">
<span>رزرو تمدید ({toPersian(refillPercent)}٪ پاداش خرید بعد):</span>
<span className="font-vazir">{toPersian(Math.round(getSubtotal() * (refillPercent / 100)).toLocaleString())} ت</span>
</div>
)}
{charityDonation > 0 && (
<div className="flex justify-between text-pink-400 font-bold">
<span>ردپای مهربانی</span>
<span className="font-vazir">{toPersian(charityDonation.toLocaleString())} تومان+</span>
</div>
)}
<div className="flex justify-between text-white/80">
<span>هزینه ارسال</span>
{shippingFee > 0 ? (
<span className="font-vazir text-white">{toPersian(shippingFee.toLocaleString())} تومان</span>
) : (
<span className="text-emerald-400 font-bold">رایگان</span>
)}
</div>
<div className="flex justify-between items-center pt-3 border-t border-white/10">
<span className="text-sm font-black text-white">مبلغ قابل پرداخت:</span>
<div className="text-left">
<span className="text-2xl font-black text-canina-gold leading-none font-vazir">
{toPersian((getTotal() + shippingFee).toLocaleString())}
</span>
<span className="text-[10px] text-white/60 font-bold mr-1">تومان</span>
</div>
</div>
</div>
{/* Discount Code Input */}
<div className="pt-1">
<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="کد تخفیف..."
className="flex-1 bg-white/10 border border-white/10 rounded-xl px-3 py-2 text-xs focus:ring-1 focus:ring-canina-blue outline-none text-white placeholder-white/40 font-mono"
/>
<button type="submit" className="bg-white/15 px-3 py-2 rounded-xl text-xs font-bold hover:bg-white/25 transition-all cursor-pointer">اعمال</button>
</form>
</div>
{/* Submit Button */}
<button
onClick={handleFinalize}
disabled={loading}
className="w-full bg-canina-blue hover:bg-blue-600 text-white py-3.5 rounded-xl font-black text-sm transition-all shadow-md shadow-canina-blue/20 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer mt-2"
>
{loading ? (
<>
<div className="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin" />
در حال ثبت...
</>
) : (
<>
<span>پرداخت و تکمیل سفارش</span>
<ArrowRight className="w-4 h-4 rotate-180" />
</>
)}
</button>
<div className="flex items-center justify-center gap-1.5 text-[10px] font-bold text-white/30 tracking-wide pt-1">
<ShieldCheck className="w-3.5 h-3.5 text-emerald-400" />
<span>تضمین امنیت پرداخت و اصالت کالا</span>
</div>
</div>
</div>
</div>
</div>
<TopUpModal
isOpen={showTopUpModal}
initialAmount={Math.max(0, (getTotal() + shippingFee) - (profile.walletBalance || 0))}
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>
);
}