canina/frontend/application/components/B2BPortal.tsx

225 lines
10 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 { productService } from "../lib/services/productService";
import { motion, AnimatePresence } from "motion/react";
import {
Building2,
ShoppingCart,
Search,
ChevronRight,
FileText,
Package,
CheckCircle2,
Lock,
Percent,
X
} from "lucide-react";
import SafeImage from "./SafeImage";
import { toPersian } from "../lib/utils";
interface QuickOrderItem {
product: Product;
quantity: number;
}
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 + (val as any), 0);
const totalWholesalePrice = Object.entries(quantities).reduce((sum, [id, qty]) => {
const numQty = qty as number;
const prod = products.find(p => p.id === id);
if (prod && numQty > 0) {
const basePrice = prod.priceValue || 0;
const wholesalePrice = basePrice * 0.7; // 30% discount for wholesale
return sum + (wholesalePrice * numQty);
}
return sum;
}, 0);
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">
<div className="mb-10">
<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">
این بخش اختصاصی برای ثبت سفارشات حجیم و استعلام موجودی آنی طراحی شده است. قیمت‌های نمایش داده شده، فی واحد کاتالوگ هستند.
</p>
</div>
<div className="mt-auto space-y-4">
<div className="p-4 bg-white/10 rounded-2xl flex items-center gap-3">
<FileText className="w-5 h-5 text-blue-300" />
<span className="text-xs font-bold">دریافت لیست قیمت PDF</span>
</div>
<div className="p-4 bg-white/10 rounded-2xl flex items-center gap-3">
<Package className="w-5 h-5 text-blue-300" />
<span className="text-xs font-bold">پیگیری محمولات قبلی</span>
</div>
</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;
const wholesalePrice = basePrice * 0.7; // 30% discount
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>
);
}