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