canina/frontend/application/components/B2BPortal.tsx
parsa aghaei 8d1b786a24
All checks were successful
Deploy Canina / deploy (push) Successful in 1m50s
chore(brand): rename all Persian occurrences of کانینا to کنینا across the project
2026-08-16 16:40:08 +03:30

264 lines
14 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 { 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>
);
}