fix: update ProductResults and admin panel for parapharmacy

- Updated ProductResults component with new source labels
- Added price display for parapharmacy products
- Updated admin panel to search both CIMA and Parapharmacy
- Updated getSourceBadge to handle parapharmacy sources
- Added CSS for product price display
This commit is contained in:
Antoni Nuñez Romeu
2026-07-16 15:58:35 +02:00
parent 3429b7e548
commit 3666f4dee8
3 changed files with 87 additions and 34 deletions
@@ -130,6 +130,12 @@
font-weight: 600;
}
.product-price {
color: var(--primary);
font-weight: 700;
font-size: 1.1rem;
}
.product-card-footer {
padding-top: 0.75rem;
border-top: 1px solid var(--outline-variant);
@@ -3,19 +3,27 @@ import './ProductResults.css';
const categoryLabels = {
otc: 'Sin Receta',
baby_food: 'Alimentación Infantil',
baby_milk: 'Leche de Fórmula',
baby_cereal: 'Cereales Bebé'
parapharmacy: 'Parafarmacia',
dermocosmética: 'Dermocosmética',
'Fórmulas lácteas': 'Fórmulas lácteas',
vitaminas: 'Vitaminas',
analgésicos: 'Analgésicos'
};
const sourceColors = {
cima: '#2563eb',
openfoodfacts: '#16a34a'
promofarma: '#16a34a',
docmorris: '#e11d48',
primor: '#7c3aed',
parapharmacy: '#16a34a'
};
const sourceLabels = {
cima: 'CIMA',
openfoodfacts: 'Open Food Facts'
promofarma: 'Promofarma',
docmorris: 'DocMorris',
primor: 'Primor',
parapharmacy: 'Parafarmacia'
};
function ProductResults({ products, onSelect }) {
@@ -95,6 +103,9 @@ function ProductCard({ product, onSelect }) {
{product.brand && (
<p><strong>Marca:</strong> {product.brand}</p>
)}
{product.price != null && (
<p className="product-price"><strong>Precio:</strong> {product.price} €</p>
)}
{product.source === 'cima' && product.active_ingredient && (
<p><strong>Principio Activo:</strong> {product.active_ingredient}</p>
)}
@@ -47,7 +47,7 @@ function PharmacyProductLink() {
}
}, [selectedPharmacy]);
// Buscar productos en la API mientras el usuario escribe
// Buscar productos en ambas APIs mientras el usuario escribe
useEffect(() => {
const q = productSearch.trim();
if (q.length < 2) {
@@ -60,12 +60,27 @@ function PharmacyProductLink() {
const timeoutId = setTimeout(async () => {
setSearching(true);
try {
const response = await fetch(`/api/products/search?q=${encodeURIComponent(q)}`, {
credentials: 'include',
signal: controller.signal,
});
const data = await response.json();
setProductResults(Array.isArray(data) ? data : []);
// Search both CIMA and Parapharmacy
const [cimaRes, paraRes] = await Promise.allSettled([
fetch(`/api/products/search?q=${encodeURIComponent(q)}`, {
credentials: 'include',
signal: controller.signal,
}),
fetch(`/api/products/parapharmacy/search?q=${encodeURIComponent(q)}&limit=10`, {
credentials: 'include',
signal: controller.signal,
})
]);
const cimaData = cimaRes.status === 'fulfilled' ? await cimaRes.value.json() : [];
const paraData = paraRes.status === 'fulfilled' ? await paraRes.value.json() : { results: [] };
const allResults = [
...(Array.isArray(cimaData) ? cimaData : []),
...(paraData.results || []).map(p => ({ ...p, source: p.source || 'parapharmacy' }))
];
setProductResults(allResults);
} catch (error) {
if (error.name === 'AbortError') return;
console.error('Error searching products:', error);
@@ -116,15 +131,15 @@ function PharmacyProductLink() {
}
try {
// Build identifier: source:id (e.g., openfoodfacts:9421025231209)
const identifier = selectedProduct._id
? `${selectedProduct.source || 'openfoodfacts'}:${selectedProduct._id}`
: `${selectedProduct.source || 'openfoodfacts'}:${selectedProduct.id}`;
// Build identifier: source:id
const productId = selectedProduct._id || selectedProduct.id;
const source = selectedProduct.source || 'parapharmacy';
const identifier = `${source}:${productId}`;
const payload = {
pharmacy_id: parseInt(formData.pharmacy_id),
product_source: selectedProduct.source || 'openfoodfacts',
product_off_id: selectedProduct._id || selectedProduct.id,
product_source: source,
product_off_id: productId,
product_name: selectedProduct.product_name || selectedProduct.name,
price: formData.price ? parseFloat(formData.price) : null,
stock: formData.stock ? parseInt(formData.stock) : 0
@@ -221,10 +236,28 @@ function PharmacyProductLink() {
};
const getSourceBadge = (source) => {
if (source === 'cima') {
return <span className="source-badge source-badge--cima">CIMA</span>;
}
return <span className="source-badge source-badge--off">OFF</span>;
const colors = {
cima: '#2563eb',
promofarma: '#16a34a',
docmorris: '#e11d48',
primor: '#7c3aed',
parapharmacy: '#16a34a'
};
const labels = {
cima: 'CIMA',
promofarma: 'Promofarma',
docmorris: 'DocMorris',
primor: 'Primor',
parapharmacy: 'Parafarmacia'
};
return (
<span
className="source-badge"
style={{ backgroundColor: colors[source] || '#6b7280' }}
>
{labels[source] || source}
</span>
);
};
return (
@@ -284,7 +317,7 @@ function PharmacyProductLink() {
</div>
<div className="form-group">
<label>Buscar Producto (Open Food Facts) *</label>
<label>Buscar Producto (CIMA / Parafarmacia) *</label>
<input
type="text"
value={productSearch}
@@ -292,22 +325,24 @@ function PharmacyProductLink() {
setProductSearch(e.target.value);
setSelectedProduct(null);
}}
placeholder="Escribe para buscar productos..."
placeholder="Escribe para buscar medicamentos o productos de parafarmacia..."
required
/>
{searching && <p className="loading-text">Buscando...</p>}
{productResults.length > 0 && !selectedProduct && (
<div className="medicine-search-results">
{productResults.slice(0, 10).map((product) => (
<div
key={product._id || product.id}
<div
key={product._id || product.id || product.nregistro}
className="search-result-item"
onClick={() => selectProduct(product)}
>
<strong>{product.product_name || product.name}</strong>
{product.brand && <span> - {product.brand}</span>}
{product.brands && <span> - {product.brands}</span>}
{product.quantity && <span> ({product.quantity})</span>}
{product.price != null && <span> - {product.price}€</span>}
{getSourceBadge(product.source || 'parapharmacy')}
</div>
))}
</div>
@@ -317,15 +352,16 @@ function PharmacyProductLink() {
<div className="selected-medicine-info">
<p>✅ Selected: <strong>{selectedProduct.product_name || selectedProduct.name}</strong></p>
<p className="medicine-details">
{selectedProduct.brand && `Marca: ${selectedProduct.brand} • `}
{selectedProduct.brands && `Marca: ${selectedProduct.brands} • `}
{selectedProduct.quantity && `Cantidad: ${selectedProduct.quantity} • `}
{getSourceBadge(selectedProduct.source || 'openfoodfacts')}
{selectedProduct.price != null && `Precio: ${selectedProduct.price}€ • `}
{getSourceBadge(selectedProduct.source || 'parapharmacy')}
{' '}
{selectedProduct._id || selectedProduct.id}
{selectedProduct._id || selectedProduct.id || selectedProduct.nregistro}
</p>
<button
type="button"
className="btn-small"
<button
type="button"
className="btn-small"
onClick={() => {
setSelectedProduct(null);
setProductSearch('');