Feat/parapharmacy api #38

Merged
Ichitux merged 25 commits from feat/parapharmacy-api into main 2026-07-16 15:38:59 +00:00
3 changed files with 87 additions and 34 deletions
Showing only changes of commit 3666f4dee8 - Show all commits
@@ -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('');