- Fix seed-products.ts TS error (implicit any) and BOM handling - Re-run full seed to restore correct Persian encoding in DB - Fix productService query→search param mismatch - Fix IngredientWiki hardcoded port 4000→use settingsStore - Restore seed-products-data.json from git after accidental corruption
193 lines
8.6 KiB
TypeScript
193 lines
8.6 KiB
TypeScript
"use client";
|
||
import React, { useState, useEffect } from "react";
|
||
import { Product } from "../lib/data/products";
|
||
import { useCartStore } from "../lib/store/cartStore";
|
||
import { productService } from "../lib/services/productService";
|
||
import { motion, AnimatePresence } from "motion/react";
|
||
import {
|
||
Building2,
|
||
ShoppingCart,
|
||
Search,
|
||
ChevronRight,
|
||
FileText,
|
||
Package,
|
||
CheckCircle2,
|
||
X
|
||
} from "lucide-react";
|
||
import SafeImage from "./SafeImage";
|
||
|
||
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 [showSuccess, setShowSuccess] = useState(false);
|
||
|
||
useEffect(() => {
|
||
productService.getProducts().then(data => setProducts(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);
|
||
|
||
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>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-medical-gray-100">
|
||
{filteredProducts.map(product => (
|
||
<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">
|
||
<span className="inline-flex items-center gap-1.5 px-3 py-1 bg-green-50 text-green-600 rounded-full text-[10px] font-bold">
|
||
<CheckCircle2 className="w-3 h-3" />
|
||
آماده ارسال
|
||
</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>
|
||
);
|
||
}
|