canina/frontend/application/components/B2BPortal.tsx

274 lines
13 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,
Download
} 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);
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">
<h4 className="text-xs font-black mb-2 text-amber-300">درخواست تایید حساب همکار</h4>
<p className="text-[11px] text-white/80 mb-3">جهت مشاهده و خرید با قیمت عمده، شماره پروانه نظام دامپزشکی خود را وارد کنید.</p>
<form onSubmit={async (e) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const bName = (form.elements.namedItem('bName') as HTMLInputElement).value;
const lNum = (form.elements.namedItem('lNum') as HTMLInputElement).value;
try {
const api = (await import('../lib/services/api')).default;
await api.post('/wholesale/apply', { businessName: bName, licenseNumber: lNum });
alert('درخواست ثبت‌نام همکار با موفقیت ارسال شد و به زودی توسط ادمین تایید می‌شود.');
form.reset();
} catch (err) {
alert('لطفاً ابتدا وارد حساب کاربری خود شوید.');
}
}} className="space-y-2">
<input name="bName" required placeholder="نام کلینیک / داروخانه" className="w-full bg-white/20 text-white text-xs px-3 py-2 rounded-xl outline-none placeholder:text-white/50" />
<input name="lNum" required placeholder="شماره پروانه / نظام" className="w-full bg-white/20 text-white text-xs px-3 py-2 rounded-xl outline-none placeholder:text-white/50" />
<button type="submit" className="w-full bg-amber-400 hover:bg-amber-500 text-gray-900 font-black text-xs py-2 rounded-xl transition-colors">
ثبت درخواست همکار
</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;
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>
);
}