- Backend Node.js/Express con PostgreSQL - Frontend React 19 con Vite - Docker Compose para orquestacion - Documentacion completa en README.md - Scripts SQL para base de datos - Configuracion de ejemplo (.env.example)
315 lines
11 KiB
JavaScript
315 lines
11 KiB
JavaScript
import React, { useState, useEffect, useContext } from 'react';
|
|
import '../../components/Filters/Filters.css';
|
|
import './Outcomes.css';
|
|
import { langContext } from '../../context/LenguageContext';
|
|
import ConfirmationOutcome from '../../components/Modals/ConfirmationOutcome';
|
|
import axios from 'axios';
|
|
|
|
const IMAGE_BASE_URL = import.meta.env.VITE_API_BASE_URL + '/products';
|
|
|
|
|
|
export default function HousekeeperOutcomes() {
|
|
const { lang } = useContext(langContext);
|
|
const [products, setProducts] = useState([]);
|
|
const [filteredProducts, setFilteredProducts] = useState([]);
|
|
const [categories, setCategories] = useState([]);
|
|
const [nameFilter, setNameFilter] = useState('');
|
|
const [categoryFilter, setCategoryFilter] = useState('');
|
|
const [stockFilter, setStockFilter] = useState('');
|
|
const [formHousekepeer, setFormHousekepeer] = useState(null)
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
const [selectedId, setSelectedId] = useState(null);
|
|
const [selectedProdName, setSelectedProdName] = useState(null);
|
|
const [selectedProductStock, setSelectedProductStock] = useState(null);
|
|
|
|
const handleOpenModal = (id, product, productStock) => {
|
|
setSelectedId(id);
|
|
setSelectedProdName(product);
|
|
setSelectedProductStock(productStock);
|
|
//setSelectedStatus(statusType);
|
|
//setSelectedStatusId(statusId);
|
|
setModalOpen(true);
|
|
};
|
|
const handleConfirm = (PCO, UCO, HCO) => {
|
|
if (!PCO || !UCO || !HCO) return;
|
|
|
|
const currentdate = new Date();
|
|
axios.post(`${import.meta.env.VITE_API_BASE_URL}/products/newconsumptionstock`, {
|
|
"product_id": PCO,
|
|
"quantity_consumption": UCO,
|
|
"date_consumption": currentdate.getFullYear() + "-" + (currentdate.getMonth() + 1) + "-" + currentdate.getDate(),
|
|
"rfc_emp": HCO
|
|
})
|
|
.then((res) => {
|
|
console.log(res || 'Outcome realizado correctamente');
|
|
// Cerrar el modal y limpiar selección
|
|
setModalOpen(false);
|
|
setSelectedId(null);
|
|
setSelectedProdName(null);
|
|
setSelectedProductStock(null);
|
|
async function loadProducts() {
|
|
try {
|
|
const response = await fetch(import.meta.env.VITE_API_BASE_URL + '/products');
|
|
const data = await response.json();
|
|
const productsList = Array.isArray(data.data) ? data.data : [];
|
|
|
|
const productsWithDetails = await Promise.all(
|
|
productsList.map(async (prod) => {
|
|
try {
|
|
const detailResponse = await fetch(
|
|
`${import.meta.env.VITE_API_BASE_URL}/products/product/${prod.id_product}`,
|
|
{ method: "PUT" }
|
|
);
|
|
const detailData = await detailResponse.json();
|
|
if (detailData.data && detailData.data.length > 0) {
|
|
const detail = detailData.data[0];
|
|
return {
|
|
...prod,
|
|
category: detail.id_category_pro || null,
|
|
};
|
|
}
|
|
return {
|
|
...prod,
|
|
category: null,
|
|
};
|
|
} catch (err) {
|
|
console.error(`Error fetching product ${prod.id_product}:`, err);
|
|
return {
|
|
...prod,
|
|
category: null,
|
|
};
|
|
}
|
|
})
|
|
);
|
|
|
|
setProducts(productsWithDetails);
|
|
setFilteredProducts(productsWithDetails);
|
|
} catch (err) {
|
|
console.error('Error fetching products', err);
|
|
setProducts([]);
|
|
setFilteredProducts([]);
|
|
}
|
|
}
|
|
loadProducts();
|
|
})
|
|
.catch((err) => {
|
|
console.error(`Error al actualizar el outcome con el producto ${selectedId}:`, err);
|
|
alert('❌ Error al actualizar el outcome.');
|
|
});
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
setModalOpen(false);
|
|
};
|
|
|
|
useEffect(() => {
|
|
async function loadProducts() {
|
|
try {
|
|
const response = await fetch(import.meta.env.VITE_API_BASE_URL + '/products');
|
|
const data = await response.json();
|
|
const productsList = Array.isArray(data.data) ? data.data : [];
|
|
|
|
const productsWithDetails = await Promise.all(
|
|
productsList.map(async (prod) => {
|
|
try {
|
|
const detailResponse = await fetch(
|
|
`${import.meta.env.VITE_API_BASE_URL}/products/product/${prod.id_product}`,
|
|
{ method: "PUT" }
|
|
);
|
|
const detailData = await detailResponse.json();
|
|
if (detailData.data && detailData.data.length > 0) {
|
|
const detail = detailData.data[0];
|
|
return {
|
|
...prod,
|
|
category: detail.id_category_pro || null,
|
|
};
|
|
}
|
|
return {
|
|
...prod,
|
|
category: null,
|
|
};
|
|
} catch (err) {
|
|
console.error(`Error fetching product ${prod.id_product}:`, err);
|
|
return {
|
|
...prod,
|
|
category: null,
|
|
};
|
|
}
|
|
})
|
|
);
|
|
|
|
setProducts(productsWithDetails);
|
|
setFilteredProducts(productsWithDetails);
|
|
} catch (err) {
|
|
console.error('Error fetching products', err);
|
|
setProducts([]);
|
|
setFilteredProducts([]);
|
|
}
|
|
}
|
|
async function fetchSelectData() {
|
|
try {
|
|
const res = await fetch(import.meta.env.VITE_API_BASE_URL + '/products/gethousekeeper', {
|
|
method: 'GET',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
if (!res.ok) throw new Error(`Error fetching info: ${res.status}`);
|
|
const data = await res.json();
|
|
setFormHousekepeer(data.houseKeeper || []);
|
|
} catch (err) {
|
|
console.error('Error cargando metadata (getinfo):', err);
|
|
}
|
|
}
|
|
fetchSelectData();
|
|
loadProducts();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
async function loadFilterOptions() {
|
|
try {
|
|
const categoriesRes = await fetch(
|
|
import.meta.env.VITE_API_BASE_URL + "/products/productcategory"
|
|
);
|
|
|
|
const categoriesData = await categoriesRes.json();
|
|
|
|
if (categoriesData.data) {
|
|
setCategories(
|
|
categoriesData.data
|
|
.filter((d) => d.id_prod_category)
|
|
.map((d) => ({
|
|
id: d.id_prod_category,
|
|
name: d.name_prod_category,
|
|
spanish_name: d.spanish_name || d.name_prod_category,
|
|
}))
|
|
);
|
|
}
|
|
} catch (err) {
|
|
console.error("Error loading filter options", err);
|
|
}
|
|
}
|
|
loadFilterOptions();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
let filtered = [...products];
|
|
|
|
if (nameFilter) {
|
|
filtered = filtered.filter(prod =>
|
|
prod.name_product?.toLowerCase().includes(nameFilter.toLowerCase())
|
|
);
|
|
}
|
|
|
|
if (categoryFilter) {
|
|
filtered = filtered.filter(
|
|
(prod) => prod.category && prod.category.toString() === categoryFilter
|
|
);
|
|
}
|
|
|
|
if (stockFilter) {
|
|
switch (stockFilter) {
|
|
case 'out':
|
|
filtered = filtered.filter(prod => (parseInt(prod.units) || 0) === 0);
|
|
break;
|
|
case 'low':
|
|
filtered = filtered.filter(prod => {
|
|
const units = parseInt(prod.units) || 0;
|
|
return units > 0 && units < 10;
|
|
});
|
|
break;
|
|
case 'in':
|
|
filtered = filtered.filter(prod => (parseInt(prod.units) || 0) >= 10);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
setFilteredProducts(filtered);
|
|
}, [nameFilter, categoryFilter, stockFilter, products]);
|
|
|
|
return (
|
|
<div className="outcomes-page">
|
|
<div className="page-header">
|
|
<h2 className="page-title">{lang === "es" ? "Productos" : "Products"}</h2>
|
|
</div>
|
|
|
|
<div className="filters-section">
|
|
<input
|
|
type="text"
|
|
className="filter-search"
|
|
placeholder={lang === 'es' ? 'Search by name...' : 'Search by name...'}
|
|
value={nameFilter}
|
|
onChange={(e) => setNameFilter(e.target.value)}
|
|
/>
|
|
|
|
<select
|
|
value={categoryFilter}
|
|
onChange={(e) => setCategoryFilter(e.target.value)}
|
|
className="filter-select"
|
|
>
|
|
<option value="">
|
|
{lang === "es" ? "Todas las categorías" : "All Categories"}
|
|
</option>
|
|
{categories
|
|
.sort((a, b) => {
|
|
const nameA = lang === "en" ? a.name : a.spanish_name;
|
|
const nameB = lang === "en" ? b.name : b.spanish_name;
|
|
return nameA.localeCompare(nameB);
|
|
})
|
|
.map((cat, index) => (
|
|
<option key={index} value={cat.id}>
|
|
{lang === "en" ? cat.name : cat.spanish_name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
|
|
<select
|
|
value={stockFilter}
|
|
onChange={(e) => setStockFilter(e.target.value)}
|
|
className="filter-select"
|
|
>
|
|
<option value="">{lang === "es" ? "Todo el stock" : "All Stock"}</option>
|
|
<option value="out">{lang === "es" ? "Sin stock" : "Out of Stock"}</option>
|
|
<option value="low">{lang === "es" ? "Stock bajo (< 10)" : "Low Stock (< 10)"}</option>
|
|
<option value="in">{lang === "es" ? "En stock (≥ 10)" : "In Stock (≥ 10)"}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="products-grid">
|
|
{Array.isArray(filteredProducts) &&
|
|
filteredProducts.map((prod, index) => (
|
|
<div key={prod.id_product || index} className="product-card" onClick={() => handleOpenModal(prod.id_product, prod.name_product, prod.units)}>
|
|
{prod.image_product && (
|
|
<img
|
|
src={
|
|
prod.image_product.startsWith('http')
|
|
? prod.image_product
|
|
: prod.image_product.length > 200
|
|
? `data:image/jpeg;base64,${prod.image_product}`
|
|
: `${IMAGE_BASE_URL}/${prod.image_product}`
|
|
}
|
|
alt={prod.name_product}
|
|
className="product-image"
|
|
/>
|
|
)}
|
|
<div className="product-info">
|
|
<div className="product-name">{prod.name_product}</div>
|
|
<div className="product-price">{lang === "es" ? "Precio: $" : "Price: $"}{prod.price_product}</div>
|
|
<div className="product-units">{lang === "es" ? "Unidades: " : "Units: "}{prod.units}</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<ConfirmationOutcome
|
|
isOpen={modalOpen}
|
|
onConfirm={handleConfirm}
|
|
onCancel={handleCancel}
|
|
formHousekepeer={formHousekepeer}
|
|
idproduct={selectedId}
|
|
nameProduct={selectedProdName}
|
|
productStock={selectedProductStock}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|