Ir al contenido

Acciones Masivas

Operaciones en lote sobre múltiples gastos seleccionados.

src/features/expenses/components/ExpenseBulkActionsBar.tsx
<ExpenseBulkActionsBar
selectedCount={selectedIds.size}
recordedCount={recordedCount}
onVerify={handleBatchVerify}
onClearSelection={clearSelection}
isProcessing={isBatchProcessing}
/>
  • Barra flotante que aparece al seleccionar gastos
  • Contador visual de gastos seleccionados
  • Botón de verificar para gastos en estado “recorded”
  • Botón de deseleccionar para limpiar selección
  • Feedback visual durante el procesamiento

Marca múltiples gastos como verificados:

const handleBatchVerify = async () => {
// Filtrar solo gastos en estado 'recorded'
const recordedSelected = Array.from(selectedIds).filter(id => {
const expense = expenses.find(e => e.id === id);
return expense && expense.status === 'recorded';
});
if (recordedSelected.length === 0) return;
setIsBatchProcessing(true);
let successCount = 0;
// Procesar uno por uno
for (const id of recordedSelected) {
try {
await supabase
.from('expenses')
.update({
status: 'verified',
verified_at: new Date().toISOString(),
verified_by: user.id,
})
.eq('id', id);
successCount++;
} catch (error) {
console.error('Error verifying expense:', id, error);
}
}
setIsBatchProcessing(false);
clearSelection();
toast({
title: "Gastos verificados",
description: `${successCount} ${successCount === 1 ? 'gasto verificado' : 'gastos verificados'} correctamente`,
});
// Recargar lista
loadExpenses();
};
// Estado de selección
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
// Toggle individual
const toggleSelection = (id: string) => {
setSelectedIds(prev => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
};
// Toggle todos (solo en página actual)
const toggleSelectAll = () => {
const recordedInPage = paginatedData.filter(e => e.status === 'recorded');
const allSelected = recordedInPage.every(e => selectedIds.has(e.id));
if (allSelected) {
setSelectedIds(prev => {
const next = new Set(prev);
recordedInPage.forEach(e => next.delete(e.id));
return next;
});
} else {
setSelectedIds(prev => {
const next = new Set(prev);
recordedInPage.forEach(e => next.add(e.id));
return next;
});
}
};
// Limpiar selección
const clearSelection = () => setSelectedIds(new Set());
<TableRow
key={expense.id}
className={selectedIds.has(expense.id) ? 'bg-primary/5' : ''}
>
<TableCell>
{expense.status === 'recorded' ? (
<Checkbox
checked={selectedIds.has(expense.id)}
onCheckedChange={() => toggleSelection(expense.id)}
/>
) : (
<div className="w-4" />
)}
</TableCell>
{/* ... resto de celdas */}
</TableRow>
// Calcular cuántos seleccionados pueden verificarse
const recordedCount = Array.from(selectedIds).filter(id => {
const expense = expenses.find(e => e.id === id);
return expense && expense.status === 'recorded';
}).length;
// Mostrar en barra de acciones
<span className="font-medium">
{selectedIds.size} {selectedIds.size === 1 ? 'gasto seleccionado' : 'gastos seleccionados'}
</span>
{recordedCount > 0 && (
<Button onClick={onVerify}>
Verificar ({recordedCount})
</Button>
)}
  • Barra animada: Aparece desde abajo con animación
  • Posición fija: Flotante en la parte inferior de la pantalla
  • Responsive: Se adapta a móvil con texto compacto
  • Feedback visual: Filas seleccionadas con fondo resaltado
  • Estado de procesamiento: Botones deshabilitados durante operación
  • Solo se pueden verificar gastos en estado “recorded”
  • Gastos ya verificados no aparecen como seleccionables
  • Las acciones afectan solo a los gastos de la página actual (con paginación)
  • El procesamiento es secuencial para evitar rate limits