856 lines
48 KiB
TypeScript
856 lines
48 KiB
TypeScript
"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 [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 [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) {
|
||
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: 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 (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-6 sm:py-12 px-3 sm:px-6 font-vazir w-full max-w-full overflow-x-hidden" dir="rtl">
|
||
<div className="max-w-7xl mx-auto w-full">
|
||
|
||
{/* Mobile Navigation & Breadcrumbs */}
|
||
<div className="lg:hidden mb-6 sm:mb-10 space-y-4">
|
||
<button
|
||
onClick={() => router.back()}
|
||
className="flex items-center gap-1.5 text-medical-gray-500 font-bold hover:text-canina-blue transition-colors font-vazir"
|
||
>
|
||
<span>بازگشت به سبد خرید</span>
|
||
<ChevronLeft className="w-5 h-5" />
|
||
</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-6 lg:gap-12 w-full">
|
||
{/* Main Form */}
|
||
<div className="lg:col-span-8 space-y-6 sm:space-y-8 w-full min-w-0">
|
||
{/* Step 1: Shipping */}
|
||
<section className="bg-white rounded-[2rem] sm:rounded-[3rem] border border-medical-gray-200 p-4 sm:p-10 overflow-hidden relative w-full">
|
||
<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 w-full">
|
||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 mb-2">
|
||
<p className="text-xs font-bold text-medical-gray-500">انتخاب آدرس جهت تحویل سفارش:</p>
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowAddressModal(true)}
|
||
className="self-start sm:self-auto 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 w-full">
|
||
{profile.addresses.map((addr) => (
|
||
<div
|
||
key={addr.id}
|
||
onClick={() => setSelectedAddressId(addr.id)}
|
||
className={cn(
|
||
"p-3 sm:p-4 rounded-2xl border-2 cursor-pointer transition-all flex items-start gap-2.5 sm:gap-3 w-full min-w-0 max-w-full overflow-hidden",
|
||
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 shrink-0"
|
||
/>
|
||
<div className="flex-1 min-w-0 break-words">
|
||
<div className="flex items-center gap-2 mb-1 flex-wrap">
|
||
<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 shrink-0">پیشفرض</span>
|
||
)}
|
||
</div>
|
||
<p className="text-xs text-medical-gray-600 leading-relaxed break-words line-clamp-3 sm:line-clamp-none">{addr.province}، {addr.city}، {addr.detail}</p>
|
||
<p className="text-[10px] text-medical-gray-400 font-bold mt-1 break-words">گیرنده: {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>
|
||
|
||
{/* Pet Selection */}
|
||
{pets.length > 0 && (
|
||
<section className="bg-white rounded-[2rem] sm:rounded-[3rem] border border-medical-gray-200 p-5 sm:p-8 overflow-hidden relative">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-10 h-10 bg-purple-100 text-purple-700 rounded-2xl flex items-center justify-center font-black">🐾</div>
|
||
<div>
|
||
<h2 className="text-lg font-black text-medical-gray-900 italic">انتخاب همدم (حیوان خانگی)</h2>
|
||
<p className="text-xs text-medical-gray-500">برای ثبت سابقه خرید در پرونده سلامت پت، همدم موردنظر را انتخاب کنید:</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||
<button
|
||
type="button"
|
||
onClick={() => setSelectedPetId(null)}
|
||
className={cn(
|
||
"p-3 rounded-2xl border-2 text-xs font-black transition-all flex items-center justify-center gap-2 cursor-pointer",
|
||
selectedPetId === null ? "border-canina-blue bg-canina-blue/10 text-canina-blue shadow-xs" : "border-medical-gray-100 text-medical-gray-500 hover:bg-medical-gray-50"
|
||
)}
|
||
>
|
||
<span>هیچکدام (خرید عمومی)</span>
|
||
</button>
|
||
{pets.map(p => (
|
||
<button
|
||
key={p.id}
|
||
type="button"
|
||
onClick={() => setSelectedPetId(p.id)}
|
||
className={cn(
|
||
"p-3 rounded-2xl border-2 text-xs font-black transition-all flex items-center justify-center gap-2 cursor-pointer",
|
||
selectedPetId === p.id ? "border-canina-blue bg-canina-blue/10 text-canina-blue shadow-xs" : "border-medical-gray-100 text-medical-gray-600 hover:bg-medical-gray-50"
|
||
)}
|
||
>
|
||
<span>🐾 {p.name} ({p.breed || p.type})</span>
|
||
</button>
|
||
))}
|
||
</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>
|
||
);
|
||
}
|