Acciones Masivas
Exportar contenido
Operaciones en lote sobre múltiples gastos seleccionados.
Componente
Sección titulada «Componente»<ExpenseBulkActionsBar selectedCount={selectedIds.size} recordedCount={recordedCount} onVerify={handleBatchVerify} onClearSelection={clearSelection} isProcessing={isBatchProcessing}/>Características
Sección titulada «Características»- 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
Operaciones disponibles
Sección titulada «Operaciones disponibles»Verificar en lote
Sección titulada «Verificar en lote»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();};Selección de gastos
Sección titulada «Selección de gastos»// Estado de selecciónconst [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
// Toggle individualconst 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ónconst clearSelection = () => setSelectedIds(new Set());Checkbox en tabla
Sección titulada «Checkbox en tabla»<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>Estadísticas de selección
Sección titulada «Estadísticas de selección»// Calcular cuántos seleccionados pueden verificarseconst 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>)}Comportamiento UX
Sección titulada «Comportamiento UX»- 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
Restricciones
Sección titulada «Restricciones»- 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