Skip to content

Conversation

@tomaskoblukUTN
Copy link
Contributor

No description provided.

Copilot AI review requested due to automatic review settings December 4, 2025 17:17
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds comprehensive filtering and search capabilities to the inventory management page. The implementation includes a collapsible filter panel with price range, stock range, and category filters, along with a search bar for filtering products by name.

Key Changes:

  • Added search functionality with real-time filtering by product name
  • Implemented advanced filters for price range, stock quantity, and category selection
  • Introduced a toggleable filter panel UI with responsive design and improved styling
Comments suppressed due to low confidence (1)

apigrupo11/frontend/src/app/inventario/page.tsx:128

  • The filter logic doesn't handle the case where minPrice > maxPrice or minStock > maxStock. Users could set minPrice to 1000 and maxPrice to 100, which would result in no products being displayed without clear feedback about the invalid range.

Consider adding validation:

const filteredProductos = productos.filter((producto) => {
  const matchesSearch = producto.nombre
    .toLowerCase()
    .includes(searchQuery.toLowerCase());
  
  // Swap if min > max
  const actualMinPrice = Math.min(appliedFilters.minPrice, appliedFilters.maxPrice);
  const actualMaxPrice = Math.max(appliedFilters.minPrice, appliedFilters.maxPrice);
  const matchesPrice =
    producto.precio >= actualMinPrice &&
    producto.precio <= actualMaxPrice;
  
  // Similar for stock...
  const matchesStock = /* ... */;
  const matchesCategory = /* ... */;

  return matchesSearch && matchesPrice && matchesStock && matchesCategory;
});

Or prevent invalid ranges in the handleApplyFilters function.

  const handleUpdateProduct = (updatedProduct: IProducto) => {
    setProductos((prev) => prev.map((p) => (p.id === updatedProduct.id ? updatedProduct : p)));
    setShowAddForm(false);
    setEditingProduct(null);
  };

  const handleEditClick = (product: IProducto) => {
    setEditingProduct(product);
    setShowAddForm(true);
  };

  const handleCloseForm = () => {
    setShowAddForm(false);
    setEditingProduct(null);
  };


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 317 to 630
margin: 0,
display: 'flex',
alignItems: 'center',
gap: '8px',
}}
>
<MdFilterList size={20} />
Filtrar productos
</h3>
<p
style={{
fontSize: 'clamp(12px, 3vw, 13px)',
color: theme.colors.textSecondary,
margin: '4px 0 0 0',
}}
>
Ajusta los filtros y presiona Aplicar
</p>
</div>

{/* Grid de filtros - responsive */}
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))',
gap: 'clamp(16px, 3%, 24px)',
marginBottom: '24px',
}}
>
{/* Rango de Precio */}
<div>
<label
style={{
color: theme.colors.textPrimary,
fontSize: 'clamp(12px, 4vw, 14px)',
fontWeight: 600,
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '12px',
}}
>
<MdAttachMoney size={18} />
Rango de Precio
</label>
<p
style={{
color: theme.colors.textSecondary,
fontSize: 'clamp(11px, 3vw, 12px)',
margin: '0 0 8px 0',
}}
>
${filters.minPrice} - ${filters.maxPrice}
</p>
<div
style={{
display: 'flex',
gap: 'clamp(8px, 2%, 12px)',
flexWrap: 'wrap',
}}
>
<input
type="number"
placeholder="$0"
step="1"
value={filters.minPrice || ''}
onChange={(e) =>
setFilters({
...filters,
minPrice:
e.target.value === '' ? 0 : Number(e.target.value),
})
}
style={{
flex: 1,
minWidth: '80px',
padding: 'clamp(8px, 2%, 12px) clamp(10px, 2%, 14px)',
borderRadius: theme.borderRadius.sm,
border: `2px solid ${theme.colors.border}`,
backgroundColor: theme.colors.darkBgSecondary,
color: theme.colors.textPrimary,
fontSize: 'clamp(12px, 3vw, 14px)',
fontWeight: 500,
transition: 'all 0.2s ease',
outline: 'none',
boxSizing: 'border-box',
}}
onFocus={(e) => {
(e.target as any).style.borderColor =
theme.colors.primary;
(
e.target as any
).style.boxShadow = `0 0 0 3px ${theme.colors.primary}20`;
}}
onBlur={(e) => {
(e.target as any).style.borderColor = theme.colors.border;
(e.target as any).style.boxShadow = 'none';
}}
/>
<input
type="number"
placeholder="$10000"
step="1"
value={filters.maxPrice || ''}
onChange={(e) =>
setFilters({
...filters,
maxPrice:
e.target.value === ''
? 10000
: Number(e.target.value),
})
}
style={{
flex: 1,
minWidth: '80px',
padding: 'clamp(8px, 2%, 12px) clamp(10px, 2%, 14px)',
borderRadius: theme.borderRadius.sm,
border: `2px solid ${theme.colors.border}`,
backgroundColor: theme.colors.darkBgSecondary,
color: theme.colors.textPrimary,
fontSize: 'clamp(12px, 3vw, 14px)',
fontWeight: 500,
transition: 'all 0.2s ease',
outline: 'none',
boxSizing: 'border-box',
}}
onFocus={(e) => {
(e.target as any).style.borderColor =
theme.colors.primary;
(
e.target as any
).style.boxShadow = `0 0 0 3px ${theme.colors.primary}20`;
}}
onBlur={(e) => {
(e.target as any).style.borderColor = theme.colors.border;
(e.target as any).style.boxShadow = 'none';
}}
/>
</div>
</div>

{/* Rango de Cantidad */}
<div>
<label
style={{
color: theme.colors.textPrimary,
fontSize: 'clamp(12px, 4vw, 14px)',
fontWeight: 600,
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '12px',
}}
>
<MdInventory2 size={18} />
Rango de Cantidad
</label>
<p
style={{
color: theme.colors.textSecondary,
fontSize: 'clamp(11px, 3vw, 12px)',
margin: '0 0 8px 0',
}}
>
{filters.minStock} - {filters.maxStock} unidades
</p>
<div
style={{
display: 'flex',
gap: 'clamp(8px, 2%, 12px)',
flexWrap: 'wrap',
}}
>
<input
type="number"
placeholder="0"
step="1"
value={filters.minStock || ''}
onChange={(e) =>
setFilters({
...filters,
minStock:
e.target.value === '' ? 0 : Number(e.target.value),
})
}
style={{
flex: 1,
minWidth: '80px',
padding: 'clamp(8px, 2%, 12px) clamp(10px, 2%, 14px)',
borderRadius: theme.borderRadius.sm,
border: `2px solid ${theme.colors.border}`,
backgroundColor: theme.colors.darkBgSecondary,
color: theme.colors.textPrimary,
fontSize: 'clamp(12px, 3vw, 14px)',
fontWeight: 500,
transition: 'all 0.2s ease',
outline: 'none',
boxSizing: 'border-box',
}}
onFocus={(e) => {
(e.target as any).style.borderColor =
theme.colors.primary;
(
e.target as any
).style.boxShadow = `0 0 0 3px ${theme.colors.primary}20`;
}}
onBlur={(e) => {
(e.target as any).style.borderColor = theme.colors.border;
(e.target as any).style.boxShadow = 'none';
}}
/>
<input
type="number"
placeholder="10000"
step="1"
value={filters.maxStock || ''}
onChange={(e) =>
setFilters({
...filters,
maxStock:
e.target.value === ''
? 10000
: Number(e.target.value),
})
}
style={{
flex: 1,
minWidth: '80px',
padding: 'clamp(8px, 2%, 12px) clamp(10px, 2%, 14px)',
borderRadius: theme.borderRadius.sm,
border: `2px solid ${theme.colors.border}`,
backgroundColor: theme.colors.darkBgSecondary,
color: theme.colors.textPrimary,
fontSize: 'clamp(12px, 3vw, 14px)',
fontWeight: 500,
transition: 'all 0.2s ease',
outline: 'none',
boxSizing: 'border-box',
}}
onFocus={(e) => {
(e.target as any).style.borderColor =
theme.colors.primary;
(
e.target as any
).style.boxShadow = `0 0 0 3px ${theme.colors.primary}20`;
}}
onBlur={(e) => {
(e.target as any).style.borderColor = theme.colors.border;
(e.target as any).style.boxShadow = 'none';
}}
/>
</div>
</div>

{/* Categoría */}
<div>
<label
style={{
color: theme.colors.textPrimary,
fontSize: 'clamp(12px, 4vw, 14px)',
fontWeight: 600,
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '12px',
}}
>
<MdCategory size={18} />
Categoría
</label>
<p
style={{
color: theme.colors.textSecondary,
fontSize: 'clamp(11px, 3vw, 12px)',
margin: '0 0 8px 0',
}}
>
{filters.categoria || 'Todas las categorías'}
</p>
<select
value={filters.categoria}
onChange={(e) =>
setFilters({ ...filters, categoria: e.target.value })
}
style={{
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filter inputs (price range, stock range) and select dropdown are missing associated id attributes and proper htmlFor attributes on their labels. This impacts accessibility as screen readers won't properly associate labels with their inputs.

For example:

<label htmlFor="minPrice" style={{...}}>
  <MdAttachMoney size={18} />
  Rango de Precio
</label>
<input
  id="minPrice"
  type="number"
  ...
/>

Apply similar changes to all filter inputs and the category select element.

Copilot uses AI. Check for mistakes.
Comment on lines +228 to +263
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<div>
<h1 style={{ fontSize: '1.875rem', fontWeight: 700, color: theme.colors.textPrimary }}>Inventario</h1>
<p style={{ color: theme.colors.textSecondary }}>Gestión de productos y stock</p>
<h1
style={{
fontSize: '1.875rem',
fontWeight: 700,
color: theme.colors.textPrimary,
}}
>
Inventario
</h1>
<p style={{ color: theme.colors.textSecondary }}>
Gestión de productos y stock
</p>
</div>
<div style={{ display: 'flex', gap: theme.spacing.md }}>
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "Filtros" button should include an aria-expanded attribute to indicate whether the filter panel is currently visible or hidden. This helps screen reader users understand the toggle state.

Update the button:

<button
  onClick={() => setShowFilters(!showFilters)}
  aria-expanded={showFilters}
  aria-controls="filters-panel"
  style={{...}}
>
  <MdFilterList size={18} />
  Filtros
</button>

Also add id="filters-panel" to the filters container div (line 268) for proper ARIA association.

Copilot uses AI. Check for mistakes.
Comment on lines 352 to 565
fontWeight: 600,
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '12px',
}}
>
<MdAttachMoney size={18} />
Rango de Precio
</label>
<p
style={{
color: theme.colors.textSecondary,
fontSize: 'clamp(11px, 3vw, 12px)',
margin: '0 0 8px 0',
}}
>
${filters.minPrice} - ${filters.maxPrice}
</p>
<div
style={{
display: 'flex',
gap: 'clamp(8px, 2%, 12px)',
flexWrap: 'wrap',
}}
>
<input
type="number"
placeholder="$0"
step="1"
value={filters.minPrice || ''}
onChange={(e) =>
setFilters({
...filters,
minPrice:
e.target.value === '' ? 0 : Number(e.target.value),
})
}
style={{
flex: 1,
minWidth: '80px',
padding: 'clamp(8px, 2%, 12px) clamp(10px, 2%, 14px)',
borderRadius: theme.borderRadius.sm,
border: `2px solid ${theme.colors.border}`,
backgroundColor: theme.colors.darkBgSecondary,
color: theme.colors.textPrimary,
fontSize: 'clamp(12px, 3vw, 14px)',
fontWeight: 500,
transition: 'all 0.2s ease',
outline: 'none',
boxSizing: 'border-box',
}}
onFocus={(e) => {
(e.target as any).style.borderColor =
theme.colors.primary;
(
e.target as any
).style.boxShadow = `0 0 0 3px ${theme.colors.primary}20`;
}}
onBlur={(e) => {
(e.target as any).style.borderColor = theme.colors.border;
(e.target as any).style.boxShadow = 'none';
}}
/>
<input
type="number"
placeholder="$10000"
step="1"
value={filters.maxPrice || ''}
onChange={(e) =>
setFilters({
...filters,
maxPrice:
e.target.value === ''
? 10000
: Number(e.target.value),
})
}
style={{
flex: 1,
minWidth: '80px',
padding: 'clamp(8px, 2%, 12px) clamp(10px, 2%, 14px)',
borderRadius: theme.borderRadius.sm,
border: `2px solid ${theme.colors.border}`,
backgroundColor: theme.colors.darkBgSecondary,
color: theme.colors.textPrimary,
fontSize: 'clamp(12px, 3vw, 14px)',
fontWeight: 500,
transition: 'all 0.2s ease',
outline: 'none',
boxSizing: 'border-box',
}}
onFocus={(e) => {
(e.target as any).style.borderColor =
theme.colors.primary;
(
e.target as any
).style.boxShadow = `0 0 0 3px ${theme.colors.primary}20`;
}}
onBlur={(e) => {
(e.target as any).style.borderColor = theme.colors.border;
(e.target as any).style.boxShadow = 'none';
}}
/>
</div>
</div>

{/* Rango de Cantidad */}
<div>
<label
style={{
color: theme.colors.textPrimary,
fontSize: 'clamp(12px, 4vw, 14px)',
fontWeight: 600,
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '12px',
}}
>
<MdInventory2 size={18} />
Rango de Cantidad
</label>
<p
style={{
color: theme.colors.textSecondary,
fontSize: 'clamp(11px, 3vw, 12px)',
margin: '0 0 8px 0',
}}
>
{filters.minStock} - {filters.maxStock} unidades
</p>
<div
style={{
display: 'flex',
gap: 'clamp(8px, 2%, 12px)',
flexWrap: 'wrap',
}}
>
<input
type="number"
placeholder="0"
step="1"
value={filters.minStock || ''}
onChange={(e) =>
setFilters({
...filters,
minStock:
e.target.value === '' ? 0 : Number(e.target.value),
})
}
style={{
flex: 1,
minWidth: '80px',
padding: 'clamp(8px, 2%, 12px) clamp(10px, 2%, 14px)',
borderRadius: theme.borderRadius.sm,
border: `2px solid ${theme.colors.border}`,
backgroundColor: theme.colors.darkBgSecondary,
color: theme.colors.textPrimary,
fontSize: 'clamp(12px, 3vw, 14px)',
fontWeight: 500,
transition: 'all 0.2s ease',
outline: 'none',
boxSizing: 'border-box',
}}
onFocus={(e) => {
(e.target as any).style.borderColor =
theme.colors.primary;
(
e.target as any
).style.boxShadow = `0 0 0 3px ${theme.colors.primary}20`;
}}
onBlur={(e) => {
(e.target as any).style.borderColor = theme.colors.border;
(e.target as any).style.boxShadow = 'none';
}}
/>
<input
type="number"
placeholder="10000"
step="1"
value={filters.maxStock || ''}
onChange={(e) =>
setFilters({
...filters,
maxStock:
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The input allows negative values for price through manual typing. A user can type -100 in the minPrice field even though step="1" is set. This could lead to unexpected filter behavior or UI inconsistencies.

Add a min="0" attribute to prevent negative values:

<input
  type="number"
  placeholder="$0"
  step="1"
  min="0"
  value={filters.minPrice || ''}
  ...
/>

Apply the same fix to all number inputs (maxPrice, minStock, maxStock) on lines 385, 460, and 498.

Copilot uses AI. Check for mistakes.
Comment on lines 151 to +197
} catch (err: any) {
console.error('[Inventario] Error al eliminar del backend:', err);
setDeleteError(`Error al eliminar el producto: ${err?.message || 'Error desconocido'}`);
setDeleteError(
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The handleApplyFilters function could provide better UX by collapsing the filter panel after filters are applied, allowing users to see the filtered results immediately without manually closing the panel.

Consider adding:

const handleApplyFilters = () => {
  setAppliedFilters(filters);
  setShowFilters(false); // Auto-collapse after applying
};

Copilot uses AI. Check for mistakes.
Comment on lines 190 to 738
</div>
)}

{/* Mensaje informativo cuando backend no está disponible */}
{productos.length > 0 &&
productos[0]?.id === 1 &&
productos[0]?.nombre === 'Laptop HP' && (
<div
style={{
backgroundColor: '#3498db20',
border: '1px solid #3498db',
borderRadius: theme.borderRadius.md,
padding: '12px 16px',
marginTop: theme.spacing.md,
color: '#3498db',
fontSize: '14px',
}}
>
ℹ️ Mostrando datos de ejemplo (backend no disponible). Para ver
datos reales, inicia el backend en puerto 3000.
</div>
)}

{/* Campo de búsqueda */}
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The search input is missing a label element or aria-label attribute. While the placeholder provides a visual hint, screen reader users need a proper label to understand the purpose of this input.

Add an accessible label:

<label htmlFor="searchInput" style={{ /* visually hidden if needed */ }}>
  Buscar producto por nombre
</label>
<input
  id="searchInput"
  type="text"
  ...
/>

Alternatively, use aria-label="Buscar producto por nombre" on the input element.

Copilot uses AI. Check for mistakes.
Comment on lines 347 to 565
<div>
<label
style={{
color: theme.colors.textPrimary,
fontSize: 'clamp(12px, 4vw, 14px)',
fontWeight: 600,
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '12px',
}}
>
<MdAttachMoney size={18} />
Rango de Precio
</label>
<p
style={{
color: theme.colors.textSecondary,
fontSize: 'clamp(11px, 3vw, 12px)',
margin: '0 0 8px 0',
}}
>
${filters.minPrice} - ${filters.maxPrice}
</p>
<div
style={{
display: 'flex',
gap: 'clamp(8px, 2%, 12px)',
flexWrap: 'wrap',
}}
>
<input
type="number"
placeholder="$0"
step="1"
value={filters.minPrice || ''}
onChange={(e) =>
setFilters({
...filters,
minPrice:
e.target.value === '' ? 0 : Number(e.target.value),
})
}
style={{
flex: 1,
minWidth: '80px',
padding: 'clamp(8px, 2%, 12px) clamp(10px, 2%, 14px)',
borderRadius: theme.borderRadius.sm,
border: `2px solid ${theme.colors.border}`,
backgroundColor: theme.colors.darkBgSecondary,
color: theme.colors.textPrimary,
fontSize: 'clamp(12px, 3vw, 14px)',
fontWeight: 500,
transition: 'all 0.2s ease',
outline: 'none',
boxSizing: 'border-box',
}}
onFocus={(e) => {
(e.target as any).style.borderColor =
theme.colors.primary;
(
e.target as any
).style.boxShadow = `0 0 0 3px ${theme.colors.primary}20`;
}}
onBlur={(e) => {
(e.target as any).style.borderColor = theme.colors.border;
(e.target as any).style.boxShadow = 'none';
}}
/>
<input
type="number"
placeholder="$10000"
step="1"
value={filters.maxPrice || ''}
onChange={(e) =>
setFilters({
...filters,
maxPrice:
e.target.value === ''
? 10000
: Number(e.target.value),
})
}
style={{
flex: 1,
minWidth: '80px',
padding: 'clamp(8px, 2%, 12px) clamp(10px, 2%, 14px)',
borderRadius: theme.borderRadius.sm,
border: `2px solid ${theme.colors.border}`,
backgroundColor: theme.colors.darkBgSecondary,
color: theme.colors.textPrimary,
fontSize: 'clamp(12px, 3vw, 14px)',
fontWeight: 500,
transition: 'all 0.2s ease',
outline: 'none',
boxSizing: 'border-box',
}}
onFocus={(e) => {
(e.target as any).style.borderColor =
theme.colors.primary;
(
e.target as any
).style.boxShadow = `0 0 0 3px ${theme.colors.primary}20`;
}}
onBlur={(e) => {
(e.target as any).style.borderColor = theme.colors.border;
(e.target as any).style.boxShadow = 'none';
}}
/>
</div>
</div>

{/* Rango de Cantidad */}
<div>
<label
style={{
color: theme.colors.textPrimary,
fontSize: 'clamp(12px, 4vw, 14px)',
fontWeight: 600,
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '12px',
}}
>
<MdInventory2 size={18} />
Rango de Cantidad
</label>
<p
style={{
color: theme.colors.textSecondary,
fontSize: 'clamp(11px, 3vw, 12px)',
margin: '0 0 8px 0',
}}
>
{filters.minStock} - {filters.maxStock} unidades
</p>
<div
style={{
display: 'flex',
gap: 'clamp(8px, 2%, 12px)',
flexWrap: 'wrap',
}}
>
<input
type="number"
placeholder="0"
step="1"
value={filters.minStock || ''}
onChange={(e) =>
setFilters({
...filters,
minStock:
e.target.value === '' ? 0 : Number(e.target.value),
})
}
style={{
flex: 1,
minWidth: '80px',
padding: 'clamp(8px, 2%, 12px) clamp(10px, 2%, 14px)',
borderRadius: theme.borderRadius.sm,
border: `2px solid ${theme.colors.border}`,
backgroundColor: theme.colors.darkBgSecondary,
color: theme.colors.textPrimary,
fontSize: 'clamp(12px, 3vw, 14px)',
fontWeight: 500,
transition: 'all 0.2s ease',
outline: 'none',
boxSizing: 'border-box',
}}
onFocus={(e) => {
(e.target as any).style.borderColor =
theme.colors.primary;
(
e.target as any
).style.boxShadow = `0 0 0 3px ${theme.colors.primary}20`;
}}
onBlur={(e) => {
(e.target as any).style.borderColor = theme.colors.border;
(e.target as any).style.boxShadow = 'none';
}}
/>
<input
type="number"
placeholder="10000"
step="1"
value={filters.maxStock || ''}
onChange={(e) =>
setFilters({
...filters,
maxStock:
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline styles for input elements are heavily duplicated across multiple inputs (min/max price, min/max stock). The onFocus and onBlur handlers with style manipulation are repeated identically 6 times.

Consider extracting this to a reusable styled component or a shared style object and event handlers:

const inputStyles = {
  flex: 1,
  minWidth: '80px',
  padding: 'clamp(8px, 2%, 12px) clamp(10px, 2%, 14px)',
  // ... rest of styles
};

const handleInputFocus = (e: React.FocusEvent<HTMLInputElement>) => {
  e.target.style.borderColor = theme.colors.primary;
  e.target.style.boxShadow = `0 0 0 3px ${theme.colors.primary}20`;
};

const handleInputBlur = (e: React.FocusEvent<HTMLInputElement>) => {
  e.target.style.borderColor = theme.colors.border;
  e.target.style.boxShadow = 'none';
};

Copilot uses AI. Check for mistakes.
Comment on lines 373 to 621
display: 'flex',
gap: 'clamp(8px, 2%, 12px)',
flexWrap: 'wrap',
}}
>
<input
type="number"
placeholder="$0"
step="1"
value={filters.minPrice || ''}
onChange={(e) =>
setFilters({
...filters,
minPrice:
e.target.value === '' ? 0 : Number(e.target.value),
})
}
style={{
flex: 1,
minWidth: '80px',
padding: 'clamp(8px, 2%, 12px) clamp(10px, 2%, 14px)',
borderRadius: theme.borderRadius.sm,
border: `2px solid ${theme.colors.border}`,
backgroundColor: theme.colors.darkBgSecondary,
color: theme.colors.textPrimary,
fontSize: 'clamp(12px, 3vw, 14px)',
fontWeight: 500,
transition: 'all 0.2s ease',
outline: 'none',
boxSizing: 'border-box',
}}
onFocus={(e) => {
(e.target as any).style.borderColor =
theme.colors.primary;
(
e.target as any
).style.boxShadow = `0 0 0 3px ${theme.colors.primary}20`;
}}
onBlur={(e) => {
(e.target as any).style.borderColor = theme.colors.border;
(e.target as any).style.boxShadow = 'none';
}}
/>
<input
type="number"
placeholder="$10000"
step="1"
value={filters.maxPrice || ''}
onChange={(e) =>
setFilters({
...filters,
maxPrice:
e.target.value === ''
? 10000
: Number(e.target.value),
})
}
style={{
flex: 1,
minWidth: '80px',
padding: 'clamp(8px, 2%, 12px) clamp(10px, 2%, 14px)',
borderRadius: theme.borderRadius.sm,
border: `2px solid ${theme.colors.border}`,
backgroundColor: theme.colors.darkBgSecondary,
color: theme.colors.textPrimary,
fontSize: 'clamp(12px, 3vw, 14px)',
fontWeight: 500,
transition: 'all 0.2s ease',
outline: 'none',
boxSizing: 'border-box',
}}
onFocus={(e) => {
(e.target as any).style.borderColor =
theme.colors.primary;
(
e.target as any
).style.boxShadow = `0 0 0 3px ${theme.colors.primary}20`;
}}
onBlur={(e) => {
(e.target as any).style.borderColor = theme.colors.border;
(e.target as any).style.boxShadow = 'none';
}}
/>
</div>
</div>

{/* Rango de Cantidad */}
<div>
<label
style={{
color: theme.colors.textPrimary,
fontSize: 'clamp(12px, 4vw, 14px)',
fontWeight: 600,
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '12px',
}}
>
<MdInventory2 size={18} />
Rango de Cantidad
</label>
<p
style={{
color: theme.colors.textSecondary,
fontSize: 'clamp(11px, 3vw, 12px)',
margin: '0 0 8px 0',
}}
>
{filters.minStock} - {filters.maxStock} unidades
</p>
<div
style={{
display: 'flex',
gap: 'clamp(8px, 2%, 12px)',
flexWrap: 'wrap',
}}
>
<input
type="number"
placeholder="0"
step="1"
value={filters.minStock || ''}
onChange={(e) =>
setFilters({
...filters,
minStock:
e.target.value === '' ? 0 : Number(e.target.value),
})
}
style={{
flex: 1,
minWidth: '80px',
padding: 'clamp(8px, 2%, 12px) clamp(10px, 2%, 14px)',
borderRadius: theme.borderRadius.sm,
border: `2px solid ${theme.colors.border}`,
backgroundColor: theme.colors.darkBgSecondary,
color: theme.colors.textPrimary,
fontSize: 'clamp(12px, 3vw, 14px)',
fontWeight: 500,
transition: 'all 0.2s ease',
outline: 'none',
boxSizing: 'border-box',
}}
onFocus={(e) => {
(e.target as any).style.borderColor =
theme.colors.primary;
(
e.target as any
).style.boxShadow = `0 0 0 3px ${theme.colors.primary}20`;
}}
onBlur={(e) => {
(e.target as any).style.borderColor = theme.colors.border;
(e.target as any).style.boxShadow = 'none';
}}
/>
<input
type="number"
placeholder="10000"
step="1"
value={filters.maxStock || ''}
onChange={(e) =>
setFilters({
...filters,
maxStock:
e.target.value === ''
? 10000
: Number(e.target.value),
})
}
style={{
flex: 1,
minWidth: '80px',
padding: 'clamp(8px, 2%, 12px) clamp(10px, 2%, 14px)',
borderRadius: theme.borderRadius.sm,
border: `2px solid ${theme.colors.border}`,
backgroundColor: theme.colors.darkBgSecondary,
color: theme.colors.textPrimary,
fontSize: 'clamp(12px, 3vw, 14px)',
fontWeight: 500,
transition: 'all 0.2s ease',
outline: 'none',
boxSizing: 'border-box',
}}
onFocus={(e) => {
(e.target as any).style.borderColor =
theme.colors.primary;
(
e.target as any
).style.boxShadow = `0 0 0 3px ${theme.colors.primary}20`;
}}
onBlur={(e) => {
(e.target as any).style.borderColor = theme.colors.border;
(e.target as any).style.boxShadow = 'none';
}}
/>
</div>
</div>

{/* Categoría */}
<div>
<label
style={{
color: theme.colors.textPrimary,
fontSize: 'clamp(12px, 4vw, 14px)',
fontWeight: 600,
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '12px',
}}
>
<MdCategory size={18} />
Categoría
</label>
<p
style={{
color: theme.colors.textSecondary,
fontSize: 'clamp(11px, 3vw, 12px)',
margin: '0 0 8px 0',
}}
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using (e.target as any) bypasses TypeScript's type safety. The event target is already typed as HTMLInputElement from the onChange handler, so you can safely access its style properties without type assertion.

Consider using a more type-safe approach:

onFocus={(e) => {
  e.target.style.borderColor = theme.colors.primary;
  e.target.style.boxShadow = `0 0 0 3px ${theme.colors.primary}20`;
}}

This pattern is repeated throughout the filter inputs and should be updated consistently.

Copilot uses AI. Check for mistakes.
Comment on lines +616 to +703
onFocus={(e) => {
(e.target as any).style.borderColor = theme.colors.primary;
(
e.target as any
).style.boxShadow = `0 0 0 3px ${theme.colors.primary}20`;
}}
onBlur={(e) => {
(e.target as any).style.borderColor = theme.colors.border;
(e.target as any).style.boxShadow = 'none';
}}
>
<option value="">Todas las categorías</option>
{categories.map((cat) => (
<option key={cat.id} value={cat.nombre}>
{cat.nombre}
</option>
))}
</select>
</div>
</div>

{/* Botones de acción */}
<div
style={{
display: 'flex',
gap: 'clamp(8px, 2%, 12px)',
paddingTop: '20px',
borderTop: `1px solid ${theme.colors.border}`,
flexWrap: 'wrap',
}}
>
<button
onClick={handleApplyFilters}
style={{
padding: 'clamp(8px, 2%, 12px) clamp(16px, 4%, 28px)',
background: theme.colors.primary,
color: theme.colors.textOnPrimary,
border: 'none',
borderRadius: theme.borderRadius.md,
cursor: 'pointer',
fontSize: 'clamp(12px, 3vw, 14px)',
fontWeight: 700,
transition: 'all 0.2s ease',
boxShadow: `0 4px 12px ${theme.colors.primary}40`,
display: 'flex',
alignItems: 'center',
gap: '6px',
}}
onMouseEnter={(e) => {
(e.target as any).style.transform = 'translateY(-2px)';
(
e.target as any
).style.boxShadow = `0 6px 16px ${theme.colors.primary}60`;
}}
onMouseLeave={(e) => {
(e.target as any).style.transform = 'translateY(0)';
(
e.target as any
).style.boxShadow = `0 4px 12px ${theme.colors.primary}40`;
}}
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "Aplicar Filtros" and "Limpiar" buttons use onMouseEnter and onMouseLeave for visual feedback, but these events don't work for keyboard users. Consider using :hover and :focus pseudo-classes via CSS-in-JS or adding onFocus handlers as well to ensure keyboard users get the same visual feedback.

Alternatively, consider using a CSS-in-JS solution or adding focus handlers:

onFocus={(e) => {
  e.target.style.transform = 'translateY(-2px)';
  e.target.style.boxShadow = `0 6px 16px ${theme.colors.primary}60`;
}}

Copilot uses AI. Check for mistakes.
Comment on lines +836 to +938
>
Stock
</th>
<th
style={{
padding: '12px',
textAlign: 'left',
fontSize: '12px',
fontWeight: 700,
color: theme.colors.textSecondary,
textTransform: 'uppercase',
letterSpacing: '0.5px',
}}
>
Precio
</th>
<th
style={{
padding: '12px',
textAlign: 'left',
fontSize: '12px',
fontWeight: 700,
color: theme.colors.textSecondary,
textTransform: 'uppercase',
letterSpacing: '0.5px',
}}
>
Acciones
</th>
</tr>
</thead>
<tbody>
{productos.map((producto) => (
<tr key={producto.id} style={{ borderBottom: `1px solid ${theme.colors.border}` }}>
{filteredProductos.map((producto) => (
<tr
key={producto.id}
style={{
borderBottom: `1px solid ${theme.colors.border}`,
}}
>
<td style={{ padding: '12px' }}>
<div>
<div style={{ fontSize: '14px', fontWeight: 600, color: theme.colors.textPrimary }}>{producto.nombre}</div>
<div style={{ fontSize: '13px', color: theme.colors.textSecondary }}>{producto.descripcion}</div>
<div
style={{
fontSize: '14px',
fontWeight: 600,
color: theme.colors.textPrimary,
}}
>
{producto.nombre}
</div>
<div
style={{
fontSize: '13px',
color: theme.colors.textSecondary,
}}
>
{producto.descripcion}
</div>
</div>
</td>
<td style={{ padding: '12px', fontSize: '14px', color: theme.colors.textPrimary }}>{producto.categoria}</td>
<td
style={{
padding: '12px',
fontSize: '14px',
color: theme.colors.textPrimary,
}}
>
{producto.categoria}
</td>
<td style={{ padding: '12px' }}>
<div style={{ fontSize: '14px', color: theme.colors.textPrimary }}>
<div
style={{
fontSize: '14px',
color: theme.colors.textPrimary,
}}
>
<div>Disponible: {producto.stockDisponible}</div>
<div style={{ color: theme.colors.textSecondary }}>Reservado: {producto.stockReservado}</div>
<div style={{ color: theme.colors.textSecondary }}>
Reservado: {producto.stockReservado}
</div>
</div>
</td>
<td style={{ padding: '12px', fontSize: '14px', color: theme.colors.textPrimary }}>${Number(producto.precio).toFixed(2)}</td>
<td
style={{
padding: '12px',
fontSize: '14px',
color: theme.colors.textPrimary,
}}
>
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When filters are applied and no products match the criteria, the UI shows an empty table body with no feedback to the user. This creates a confusing experience where users might think the page is broken or loading.

Add a check to display a "No products found" message:

<tbody>
  {filteredProductos.length === 0 ? (
    <tr>
      <td colSpan={5} style={{ padding: '48px', textAlign: 'center', color: theme.colors.textSecondary }}>
        No se encontraron productos que coincidan con los filtros aplicados
      </td>
    </tr>
  ) : (
    filteredProductos.map((producto) => (
      // ... existing product rows
    ))
  )}
</tbody>

Copilot uses AI. Check for mistakes.
import {
getProducts,
listCategories,
deleteProduct,
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import deleteProduct.

Suggested change
deleteProduct,

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants