diff --git a/CHANGELOG.md b/CHANGELOG.md index cf7a2f3..d6f9f35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,37 @@ O formato segue [Keep a Changelog](https://keepachangelog.com/pt-BR/). Este projeto adota [Versionamento Semântico](https://semver.org/pt-BR/). +## [1.43.3] - 2025-12-16 + +### Improved +- **Página Transações - Mobile Completo** - Lista de transações otimizada para mobile + - Cards individuais: data + badges (tipo/status) + descrição + conta + categoria + valor + - Menu dropdown com todas as ações (completar, editar, duplicar, dividir, etc.) + - Descrição original do banco visível quando diferente + - Fontes: 0.65-1rem, padding p-3, borders secundários + - Desktop mantém tabela completa + +- **Paginação Mobile** - Controles otimizados + - Mobile: Mostra apenas 3 botões (anterior, atual, próxima) + - Indicador "/ total" para contexto + - Botões maiores (36px) para facilitar toque + - Desktop: Mantém visualização de até 7 páginas + +### Fixed +- **CategorySelector - Scroll e Visualização** - Problemas em modais corrigidos + - Position fixed com z-index 100000 (acima de modais) + - Scroll sempre visível com scrollbar customizada (8px) + - Altura fixa garantindo funcionamento: 280px (mobile) / 350px (desktop) + - Largura mínima inteligente: 280px (mobile) / 320px (desktop) + - Ajuste automático de posição se ultrapassar viewport + - Margens de segurança: 10-20px das bordas + - Cores da scrollbar harmonizadas com tema escuro + - `-webkit-overflow-scrolling: touch` para iOS + - Modal com `overflow: auto` para scroll adequado + - Cálculo simplificado e robusto de posição + - Funciona perfeitamente em todos os lugares (modais e páginas) + + ## [1.43.2] - 2025-12-16 ### Improved diff --git a/VERSION b/VERSION index 6426a65..6564ab3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.43.2 +1.43.3 diff --git a/frontend/src/components/CategorySelector.jsx b/frontend/src/components/CategorySelector.jsx index f87737c..9783aa8 100644 --- a/frontend/src/components/CategorySelector.jsx +++ b/frontend/src/components/CategorySelector.jsx @@ -30,24 +30,46 @@ export default function CategorySelector({ const dropdownRef = useRef(null); const searchInputRef = useRef(null); - // Calcular posición del dropdown (arriba o abajo según espacio disponible) + // Calcular posición del dropdown const updateDropdownPosition = () => { if (buttonRef.current) { const rect = buttonRef.current.getBoundingClientRect(); - const dropdownHeight = 350; // altura aproximada del dropdown + const isMobile = window.innerWidth < 768; const viewportHeight = window.innerHeight; - const spaceBelow = viewportHeight - rect.bottom - 10; // 10px margen + + // Altura máxima do dropdown + const maxDropdownHeight = isMobile ? 280 : 350; + + // Largura mínima para exibir conteúdo completo + const minWidth = Math.max(rect.width, isMobile ? 280 : 320); + + // Ajustar left se ultrapassar viewport + let leftPosition = rect.left; + const viewportWidth = window.innerWidth; + if (leftPosition + minWidth > viewportWidth - 20) { + leftPosition = viewportWidth - minWidth - 20; + } + if (leftPosition < 10) { + leftPosition = 10; + } + + // Espaço disponível abaixo e acima + const spaceBelow = viewportHeight - rect.bottom - 10; const spaceAbove = rect.top - 10; - // Decidir si mostrar arriba o abajo - const showAbove = spaceBelow < dropdownHeight && spaceAbove > spaceBelow; + // Decidir se mostra acima ou abaixo + const showAbove = spaceBelow < maxDropdownHeight && spaceAbove > spaceBelow; + + // Calcular altura real disponível + const availableSpace = showAbove ? spaceAbove : spaceBelow; + const dropdownHeight = Math.min(maxDropdownHeight, Math.max(200, availableSpace)); setDropdownPosition({ - top: showAbove ? rect.top - Math.min(dropdownHeight, spaceAbove) : rect.bottom, - left: rect.left, - width: rect.width, + top: showAbove ? rect.top - dropdownHeight : rect.bottom, + left: leftPosition, + width: minWidth, showAbove: showAbove, - maxHeight: showAbove ? Math.min(dropdownHeight, spaceAbove) : Math.min(dropdownHeight, spaceBelow) + maxHeight: dropdownHeight }); } }; @@ -196,18 +218,24 @@ export default function CategorySelector({ {isOpen && (
{/* Búsqueda */} diff --git a/frontend/src/components/Modal.jsx b/frontend/src/components/Modal.jsx index 19072b1..2e19f20 100644 --- a/frontend/src/components/Modal.jsx +++ b/frontend/src/components/Modal.jsx @@ -66,7 +66,7 @@ const Modal = ({ className="modal show d-block" tabIndex="-1" onClick={handleBackdropClick} - style={{ zIndex: 1055 }} + style={{ zIndex: 1055, overflow: 'auto' }} >
@@ -83,7 +83,7 @@ const Modal = ({ )}
)} -
+
{children}
{footer && ( diff --git a/frontend/src/index.css b/frontend/src/index.css index caa68f3..29bf2f4 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1837,6 +1837,7 @@ input[type="color"]::-webkit-color-swatch { justify-content: center; gap: 0.375rem; margin-top: 1rem; + align-items: center; } .txn-pagination button { @@ -1869,6 +1870,23 @@ input[type="color"]::-webkit-color-swatch { cursor: not-allowed; } +/* Mobile pagination */ +.txn-pagination.mobile { + gap: 0.25rem; +} + +.txn-pagination.mobile button { + min-width: 36px; + height: 36px; + font-size: 0.8rem; +} + +.txn-pagination .page-info { + color: #94a3b8; + font-size: 0.75rem; + margin-left: 0.25rem; +} + /* Empty state */ .txn-empty { text-align: center; @@ -2822,3 +2840,41 @@ a, min-height: 50px; } } + +/* Category Selector - garantir que funcione em modais */ +.category-selector { + position: relative; + z-index: 1; +} + +.modal-body .category-selector { + overflow: visible; +} + +.category-selector .dropdown-menu { + max-width: 100%; +} + +/* Scroll customizado para dropdown de categoria */ +.category-dropdown-scroll { + scrollbar-width: thin; + scrollbar-color: rgba(100, 116, 139, 0.5) rgba(30, 41, 59, 0.3); +} + +.category-dropdown-scroll::-webkit-scrollbar { + width: 8px; +} + +.category-dropdown-scroll::-webkit-scrollbar-track { + background: rgba(30, 41, 59, 0.3); + border-radius: 4px; +} + +.category-dropdown-scroll::-webkit-scrollbar-thumb { + background: rgba(100, 116, 139, 0.5); + border-radius: 4px; +} + +.category-dropdown-scroll::-webkit-scrollbar-thumb:hover { + background: rgba(100, 116, 139, 0.7); +} diff --git a/frontend/src/pages/TransactionsByWeek.jsx b/frontend/src/pages/TransactionsByWeek.jsx index 4d137b2..e82f606 100644 --- a/frontend/src/pages/TransactionsByWeek.jsx +++ b/frontend/src/pages/TransactionsByWeek.jsx @@ -22,6 +22,9 @@ export default function Transactions() { // Estado para destacar transação const [highlightedTransactionId, setHighlightedTransactionId] = useState(null); + // Mobile detection + const [isMobile, setIsMobile] = useState(window.innerWidth < 768); + // Estados principais const [weeklyData, setWeeklyData] = useState(null); const [accounts, setAccounts] = useState([]); @@ -252,6 +255,13 @@ export default function Transactions() { loadWeeklyData(); }, [filters, page, selectedCurrency]); // eslint-disable-line react-hooks/exhaustive-deps + // Mobile resize detection + useEffect(() => { + const handleResize = () => setIsMobile(window.innerWidth < 768); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + // Efeito para destacar transação via URL useEffect(() => { const highlightId = searchParams.get('highlight'); @@ -1339,6 +1349,219 @@ export default function Transactions() { {/* Week Transactions */} {expandedWeeks[week.year_week] && (
+ {isMobile ? ( + // Mobile: Cards Layout +
+ {week.transactions.map(transaction => ( +
+
+ {/* Header: Date + Type Badge + Status */} +
+
+ {hasActiveFilters && ( + handleToggleTransaction(transaction.id)} + onClick={(e) => e.stopPropagation()} + /> + )} + + {formatDate(transaction.effective_date || transaction.planned_date)} + {transaction.is_overdue && } + +
+
+ + {transaction.type === 'credit' ? t('transactions.type.credit') : t('transactions.type.debit')} + + + {t(`transactions.status.${transaction.status}`)} + +
+
+ + {/* Description + Transfer/Reconciled badges */} +
openDetailModal(transaction)}> +
+ {transaction.is_transfer && } + {transaction.is_reconciled && } + + {transaction.description} + +
+ {transaction.original_description && transaction.original_description !== transaction.description && ( +
+ + {transaction.original_description.substring(0, 60)}{transaction.original_description.length > 60 ? '...' : ''} +
+ )} +
+ + {/* Account + Category */} +
+ + + {transaction.account?.name} + + {transaction.category && ( + + + {transaction.category.name} + + )} +
+ + {/* Amount + Actions */} +
+
+ {transaction.type === 'credit' ? '+' : '-'} + {formatCurrency(transaction.amount || transaction.planned_amount, selectedCurrency)} +
+
e.stopPropagation()}> + +
    + {/* Ações de Status */} + {transaction.status === 'pending' && ( + <> +
  • {t('transactions.status.label')}
  • +
  • + +
  • +
  • + +
  • +

  • + + )} + {transaction.status === 'completed' && ( + <> +
  • {t('transactions.status.label')}
  • +
  • + +
  • +

  • + + )} + + {/* Ações Principais */} +
  • + +
  • +
  • + +
  • +
  • + +
  • + + {/* Dividir transação */} + {!transaction.is_transfer && !transaction.split_parent_id && ( +
  • + +
  • + )} + + {/* Converter em transferência */} + {!transaction.is_transfer && !transaction.is_split_child && ( +
  • + +
  • + )} + + {/* Conciliar com passivo */} + {transaction.type === 'debit' && !transaction.is_reconciled && !transaction.is_transfer && ( +
  • + +
  • + )} + + {/* Criar Recorrência */} + {!transaction.is_transfer && !transaction.recurring_instance_id && ( +
  • + +
  • + )} + + {/* Cancelar */} + {transaction.status === 'pending' && ( +
  • + +
  • + )} + +

  • +
  • + +
  • +
+
+
+
+
+ ))} +
+ ) : ( + // Desktop: Table Layout @@ -1562,6 +1785,7 @@ export default function Transactions() { ))}
+ )} {/* Transferências Agrupadas */} {week.transfers && week.transfers.length > 0 && ( @@ -1666,27 +1890,51 @@ export default function Transactions() { {/* Pagination */} {pagination && pagination.total_pages > 1 && ( -
+
- {Array.from({ length: Math.min(pagination.total_pages, 7) }, (_, i) => { - let pageNum; - if (pagination.total_pages <= 7) { - pageNum = i + 1; - } else if (page <= 4) { - pageNum = i + 1; - } else if (page >= pagination.total_pages - 3) { - pageNum = pagination.total_pages - 6 + i; - } else { - pageNum = page - 3 + i; - } - return ( - + )} + - ); - })} + {page < pagination.total_pages && ( + + )} + {/* Info de página */} + + / {pagination.total_pages} + + + ) : ( + // Desktop: Mostrar até 7 páginas + Array.from({ length: Math.min(pagination.total_pages, 7) }, (_, i) => { + let pageNum; + if (pagination.total_pages <= 7) { + pageNum = i + 1; + } else if (page <= 4) { + pageNum = i + 1; + } else if (page >= pagination.total_pages - 3) { + pageNum = pagination.total_pages - 6 + i; + } else { + pageNum = page - 3 + i; + } + return ( + + ); + }) + )}