canina/frontend/application/components/B2BPortal.tsx
parsa aghaei 7615aa0e51 fix: resolve seed encoding, search param, and wiki data source issues
- 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
2026-07-11 15:40:49 +03:30

193 lines
8.6 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 { 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>
);
}