264 lines
14 KiB
TypeScript
264 lines
14 KiB
TypeScript
"use client";
|
||
import React, { useState, useEffect } from "react";
|
||
import { Product } from "../lib/data/products";
|
||
import { useCartStore } from "../lib/store/cartStore";
|
||
import { useUserStore } from "../lib/store/userStore";
|
||
import { useSettingsStore } from "../lib/store/settingsStore";
|
||
import { productService } from "../lib/services/productService";
|
||
import { motion, AnimatePresence } from "motion/react";
|
||
import {
|
||
Building2,
|
||
ShoppingCart,
|
||
Search,
|
||
CheckCircle2,
|
||
Lock,
|
||
Percent,
|
||
X,
|
||
Download
|
||
} from "lucide-react";
|
||
import SafeImage from "./SafeImage";
|
||
import { toPersian } from "../lib/utils";
|
||
|
||
export default function B2BPortal({ onClose }: { onClose: () => void }) {
|
||
const [products, setProducts] = useState<Product[]>([]);
|
||
const [quantities, setQuantities] = useState<Record<string, number>>({});
|
||
const [searchQuery, setSearchQuery] = useState("");
|
||
const { addItem } = useCartStore();
|
||
const { role } = useUserStore();
|
||
const [showSuccess, setShowSuccess] = useState(false);
|
||
|
||
const isWholesaleVerified = (role as string) === "User_Wholesale" || (role as string) === "ADMIN" || (role as string) === "User_Partner";
|
||
|
||
useEffect(() => {
|
||
productService.getProducts({ limit: 999 }).then(res => setProducts(res.data));
|
||
}, []);
|
||
|
||
const filteredProducts = products.filter(p =>
|
||
p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||
p.artNo.includes(searchQuery)
|
||
);
|
||
|
||
const handleQuantityChange = (id: string, val: string) => {
|
||
const num = parseInt(val) || 0;
|
||
setQuantities(prev => ({ ...prev, [id]: Math.max(0, num) }));
|
||
};
|
||
|
||
const handleAddAll = () => {
|
||
Object.entries(quantities).forEach(([id, qty]) => {
|
||
const numQty = qty as number;
|
||
if (numQty > 0) {
|
||
const product = products.find(p => p.id === id);
|
||
if (product) {
|
||
addItem(product, numQty);
|
||
}
|
||
}
|
||
});
|
||
setShowSuccess(true);
|
||
setTimeout(() => setShowSuccess(false), 3000);
|
||
};
|
||
|
||
const totalItems = Object.values(quantities).reduce((acc: number, val) => acc + Number(val || 0), 0);
|
||
|
||
const exportToCSV = () => {
|
||
const headers = ["کد کالا", "نام محصول", "دستهبندی", "قیمت تکفروشی (تومان)", "قیمت عمده (تومان)"];
|
||
const rows = products.map(p => {
|
||
const base = p.priceValue || 0;
|
||
const wholesale = Math.round(base * 0.7);
|
||
return [`"${p.artNo}"`, `"${p.name}"`, `"${p.category}"`, base, wholesale];
|
||
});
|
||
|
||
const csvContent = "\uFEFF" + [headers.join(","), ...rows.map(e => e.join(","))].join("\n");
|
||
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
|
||
const url = URL.createObjectURL(blob);
|
||
const link = document.createElement("a");
|
||
link.setAttribute("href", url);
|
||
link.setAttribute("download", `Canina_B2B_PriceList_${new Date().toISOString().slice(0, 10)}.csv`);
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
};
|
||
|
||
return (
|
||
<motion.div
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
exit={{ opacity: 0 }}
|
||
className="fixed inset-0 z-[100] bg-medical-gray-900/60 backdrop-blur-md flex items-center justify-center p-4 md:p-10 font-vazir"
|
||
dir="rtl"
|
||
>
|
||
<motion.div
|
||
initial={{ scale: 0.9, opacity: 0, y: 20 }}
|
||
animate={{ scale: 1, opacity: 1, y: 0 }}
|
||
className="bg-white w-full max-w-6xl h-[90vh] rounded-[3rem] shadow-2xl overflow-hidden flex flex-col md:flex-row relative"
|
||
>
|
||
<button
|
||
onClick={onClose}
|
||
className="absolute top-6 left-6 z-20 w-10 h-10 bg-medical-gray-100 rounded-full flex items-center justify-center text-medical-gray-500 hover:bg-red-50 hover:text-red-500 transition-all"
|
||
>
|
||
<X className="w-5 h-5" />
|
||
</button>
|
||
|
||
{/* Sidebar Info */}
|
||
<div className="w-full md:w-80 bg-canina-blue p-10 text-white flex flex-col overflow-y-auto">
|
||
<div className="mb-8">
|
||
<div className="w-16 h-16 bg-white/20 rounded-2xl flex items-center justify-center mb-6">
|
||
<Building2 className="w-8 h-8" />
|
||
</div>
|
||
<h2 className="text-2xl font-black mb-4 italic">پنل کلینیکها <br/> و داروخانهها</h2>
|
||
<p className="text-white/60 text-sm leading-relaxed mb-4">
|
||
ثبت سفارشات حجیم با قیمت همکار. کالاها مستقیماً با لایسنس آلمان ارسال میشوند.
|
||
</p>
|
||
|
||
{!isWholesaleVerified && (
|
||
<div className="bg-white/10 p-4 rounded-2xl border border-white/20 mt-4 font-vazir">
|
||
<h4 className="text-xs font-black mb-2 text-amber-300">درخواست مشاوره و عضویت همکار B2B</h4>
|
||
<p className="text-[11px] text-white/80 mb-3 leading-relaxed">
|
||
جهت دریافت شرایط همکاری و فعالسازی دسترسی خرید عمده، شماره تماس خود را وارد کنید تا کارشناسان فروش با شما تماس بگیرند.
|
||
</p>
|
||
<form onSubmit={async (e) => {
|
||
e.preventDefault();
|
||
const form = e.target as HTMLFormElement;
|
||
const bName = (form.elements.namedItem('bName') as HTMLInputElement).value;
|
||
const phone = (form.elements.namedItem('phone') as HTMLInputElement).value;
|
||
try {
|
||
const api = (await import('../lib/services/api')).default;
|
||
await api.post('/wholesale/apply', { businessName: bName, licenseNumber: phone });
|
||
alert('درخواست شما با موفقیت ثبت شد. کارشناسان پشتیبانی B2B کنینا به زودی جهت تایید تلفنی با شما تماس خواهند گرفت.');
|
||
form.reset();
|
||
} catch {
|
||
alert('خطا در ثبت درخواست. لطفاً دوباره تلاش کنید.');
|
||
}
|
||
}} className="space-y-2">
|
||
<input name="bName" required placeholder="نام مجموعه / کلینیک / داروخانه" className="w-full bg-white/20 text-white text-xs px-3 py-2.5 rounded-xl outline-none placeholder:text-white/50" />
|
||
<input name="phone" required type="tel" placeholder="شماره تماس همراه" className="w-full bg-white/20 text-white text-xs px-3 py-2.5 rounded-xl outline-none placeholder:text-white/50 dir-ltr text-right" />
|
||
<button type="submit" className="w-full bg-amber-400 hover:bg-amber-500 text-gray-900 font-black text-xs py-2.5 rounded-xl transition-colors">
|
||
ثبت درخواست تماس و تایید B2B
|
||
</button>
|
||
</form>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="mt-auto space-y-4 pt-6">
|
||
<button
|
||
onClick={exportToCSV}
|
||
className="w-full p-4 bg-white/10 hover:bg-white/20 rounded-2xl flex items-center gap-3 transition-colors text-right cursor-pointer"
|
||
>
|
||
<Download className="w-5 h-5 text-blue-300" />
|
||
<div>
|
||
<span className="text-xs font-black block">دانلود لیست قیمت (اکسل)</span>
|
||
<span className="text-[10px] text-white/60 block">خروجی CSV آماده چاپ</span>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Main Grid Area */}
|
||
<div className="flex-1 flex flex-col bg-medical-gray-50 overflow-hidden">
|
||
{/* Header & Search */}
|
||
<div className="p-8 border-b border-medical-gray-200 flex flex-col md:flex-row md:items-center justify-between gap-6 bg-white">
|
||
<div className="relative flex-1 max-w-md">
|
||
<Search className="absolute right-4 top-1/2 -translate-y-1/2 w-4 h-4 text-medical-gray-400" />
|
||
<input
|
||
type="text"
|
||
placeholder="جستجوی کد کالا یا نام محصول..."
|
||
value={searchQuery}
|
||
onChange={e => setSearchQuery(e.target.value)}
|
||
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-3 pr-11 pl-4 text-sm focus:outline-none focus:ring-2 focus:ring-canina-blue/20"
|
||
/>
|
||
</div>
|
||
|
||
<button
|
||
onClick={handleAddAll}
|
||
disabled={totalItems === 0}
|
||
className="bg-medical-gray-900 text-white px-8 py-3 rounded-2xl font-black text-sm flex items-center gap-2 hover:bg-canina-blue transition-colors disabled:opacity-30"
|
||
>
|
||
<ShoppingCart className="w-4 h-4" />
|
||
افزودن کلی ({totalItems} عدد)
|
||
</button>
|
||
</div>
|
||
|
||
{/* Table */}
|
||
<div className="flex-1 overflow-y-auto p-8">
|
||
<div className="bg-white border border-medical-gray-200 rounded-3xl overflow-hidden shadow-sm">
|
||
<table className="w-full text-right">
|
||
<thead className="bg-medical-gray-50 border-b border-medical-gray-200">
|
||
<tr>
|
||
<th className="px-6 py-4 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest">تصویر</th>
|
||
<th className="px-6 py-4 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest">نام و کد کالا</th>
|
||
<th className="px-6 py-4 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest">قیمت تکفروشی</th>
|
||
<th className="px-6 py-4 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest">قیمت عمده (همکار)</th>
|
||
<th className="px-6 py-4 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest">تعداد سفارش</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-medical-gray-100">
|
||
{filteredProducts.map(product => {
|
||
const basePrice = product.priceValue || 0;
|
||
// Priority 1: Specific product wholesalePrice, Priority 2: General settings percentage discount (default 30%)
|
||
const wholesaleDiscountPercent = Number(useSettingsStore.getState().getText('B2B_DISCOUNT_PERCENT', '30')) || 30;
|
||
const prodWholesale = (product as unknown as { wholesalePrice?: number }).wholesalePrice;
|
||
const wholesalePrice = prodWholesale
|
||
? Number(prodWholesale)
|
||
: Math.round(basePrice * (1 - wholesaleDiscountPercent / 100));
|
||
return (
|
||
<tr key={product.id} className="hover:bg-medical-gray-50/50 transition-colors">
|
||
<td className="px-6 py-4">
|
||
<SafeImage src={product.image} className="w-12 h-12" imgClassName="object-contain" alt={product.name} />
|
||
</td>
|
||
<td className="px-6 py-4">
|
||
<div className="font-black text-medical-gray-900">{product.name}</div>
|
||
<div className="text-[10px] font-vazir text-medical-gray-400 mt-1">شناسه کالا: {product.artNo}</div>
|
||
</td>
|
||
<td className="px-6 py-4 text-xs font-bold text-medical-gray-500">
|
||
{toPersian(basePrice.toLocaleString())} تومان
|
||
</td>
|
||
<td className="px-6 py-4">
|
||
{isWholesaleVerified ? (
|
||
<span className="inline-flex items-center gap-1.5 px-3 py-1 bg-amber-50 text-amber-700 rounded-full text-xs font-black">
|
||
<Percent className="w-3 h-3 text-amber-600" />
|
||
{toPersian(wholesalePrice.toLocaleString())} تومان
|
||
</span>
|
||
) : (
|
||
<span className="inline-flex items-center gap-1.5 px-3 py-1 bg-gray-100 text-gray-500 rounded-full text-[10px] font-bold">
|
||
<Lock className="w-3 h-3 text-gray-400" />
|
||
نیازمند تایید ادمین
|
||
</span>
|
||
)}
|
||
</td>
|
||
<td className="px-6 py-4">
|
||
<input
|
||
type="number"
|
||
min="0"
|
||
value={quantities[product.id] || ""}
|
||
onChange={(e) => handleQuantityChange(product.id, e.target.value)}
|
||
placeholder="0"
|
||
className="w-20 bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-2 px-3 text-center font-mono font-bold focus:ring-2 focus:ring-canina-blue/20 outline-none"
|
||
/>
|
||
</td>
|
||
</tr>
|
||
);
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<AnimatePresence>
|
||
{showSuccess && (
|
||
<motion.div
|
||
initial={{ opacity: 0, y: 50 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: 50 }}
|
||
className="absolute bottom-10 right-10 bg-green-600 text-white px-6 py-4 rounded-2xl shadow-xl flex items-center gap-3 z-50"
|
||
>
|
||
<CheckCircle2 className="w-5 h-5" />
|
||
<span className="font-bold text-sm">سفارشات با موفقیت به سبد خرید اضافه شدند.</span>
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
</motion.div>
|
||
</motion.div>
|
||
);
|
||
}
|