v1.42.0 - Mobile UX: Navegação entre semanas no calendário + Widgets colapsáveis consistentes

This commit is contained in:
marcoitaloesp-ai 2025-12-16 10:12:47 +00:00 committed by GitHub
parent 3ba4bed1c4
commit 1186faca3c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 382 additions and 339 deletions

View File

@ -1,259 +0,0 @@
╔═══════════════════════════════════════════════════════════════════════════════╗
║ DIRETRIZES DE DESENVOLVIMENTO - v4.0 ║
║ ║
║ ⚠️ ESTE ARQUIVO NÃO DEVE SER EDITADO APÓS QUALQUER COMMIT/PUSH ║
║ ⚠️ Representa o contrato de desenvolvimento desde a versão 1.27.2 ║
║ ⚠️ Substitui .DIRETRIZES_DESENVOLVIMENTO_v3 (v3.0) ║
║ ║
╚═══════════════════════════════════════════════════════════════════════════════╝
DATA DE CRIAÇÃO: 13 de Dezembro de 2025
VERSÃO INICIAL: 1.27.2
VERSÃO DAS DIRETRIZES: 4.0
STATUS: ATIVO E IMUTÁVEL
AMBIENTE: Windows (PowerShell)
═══════════════════════════════════════════════════════════════════════════════
REGRAS DE DESENVOLVIMENTO
═══════════════════════════════════════════════════════════════════════════════
───────────────────────────────────────────────────────────────────────────────
REGRA #1: CONTROLE DE VERSÃO SEMÂNTICO
───────────────────────────────────────────────────────────────────────────────
✓ Formato: MAJOR.MINOR.PATCH (exemplo: 1.27.2)
✓ Incrementar versão em CADA commit/push
✓ Manter sincronizado em: VERSION, CHANGELOG.md
Regra de Incremento:
- MAJOR (X.0.0): Mudanças incompatíveis, redesign completo
- MINOR (0.X.0): Novas funcionalidades
- PATCH (0.0.X): Correções de bugs, ajustes menores
───────────────────────────────────────────────────────────────────────────────
REGRA #2: VALIDAÇÃO OBRIGATÓRIA EM PRODUÇÃO
───────────────────────────────────────────────────────────────────────────────
✓ TODAS as mudanças devem ser testadas em https://webmoney.cnxifly.com
✓ Workflow obrigatório:
1. Editar código
2. Deploy para servidor (.\deploy.ps1)
3. Testar no domínio
4. Commit/push apenas após validação
5. Só então editar novamente
✗ PROIBIDO commit sem teste em produção
───────────────────────────────────────────────────────────────────────────────
REGRA #3: DOCUMENTAÇÃO ESSENCIAL
───────────────────────────────────────────────────────────────────────────────
Arquivos de documentação mantidos (apenas estes):
| Arquivo | Propósito | Atualizar quando |
|---------|-----------|------------------|
| VERSION | Número da versão | Cada commit |
| CHANGELOG.md | Histórico de mudanças | Cada commit |
| README.md | Visão geral do projeto | Mudanças significativas |
| ESTRUTURA_PROJETO.md | Estrutura técnica | Novos arquivos/endpoints |
| CREDENCIAIS_SERVIDOR.md | Acessos | Mudança de credenciais |
| .DIRETRIZES_DESENVOLVIMENTO_v4 | Este arquivo | NUNCA (criar nova versão) |
Arquivos de referência (não atualizar frequentemente):
- ESPECIFICACIONES_WEBMONEY.md (especificação original)
- APRENDIZADOS_TECNICOS.md (soluções de problemas)
- ROTEIRO_INSTALACAO_SERVIDOR.md (guia de instalação)
- DKIM_DNS_RECORD.txt (configuração DNS)
───────────────────────────────────────────────────────────────────────────────
REGRA #4: SCRIPTS DE DEPLOY (WINDOWS)
───────────────────────────────────────────────────────────────────────────────
✓ SEMPRE usar os scripts PowerShell de deploy:
Frontend: cd frontend; .\deploy.ps1
Backend: cd backend; .\deploy.ps1
✗ NUNCA enviar arquivos manualmente para diretórios errados
✓ Os scripts garantem o caminho correto:
- Frontend → /var/www/webmoney/frontend/dist
- Backend → /var/www/webmoney/backend
Requisitos Windows:
- PuTTY instalado (plink.exe, pscp.exe no PATH)
- Node.js e npm instalados
- PowerShell 5.1 ou superior
Deploy manual (se necessário):
# Frontend - Build e enviar
cd frontend
npm run build
plink -batch -pw Master9354 root@213.165.93.60 "rm -rf /var/www/webmoney/frontend/dist/*"
pscp -r -batch -pw Master9354 dist\* root@213.165.93.60:/var/www/webmoney/frontend/dist/
# Backend - Enviar e atualizar
cd backend
pscp -r -batch -pw Master9354 app root@213.165.93.60:/var/www/webmoney/backend/
plink -batch -pw Master9354 root@213.165.93.60 "cd /var/www/webmoney/backend && php artisan migrate --force"
───────────────────────────────────────────────────────────────────────────────
REGRA #5: CHECKLIST DE COMMIT
───────────────────────────────────────────────────────────────────────────────
Antes de CADA commit:
☑ VERSION atualizado
☑ CHANGELOG.md atualizado
☑ Deploy executado (.\deploy.ps1)
☑ Testado em webmoney.cnxifly.com
☑ Sem erros no console do navegador
☑ Mensagem de commit descritiva
───────────────────────────────────────────────────────────────────────────────
REGRA #6: PROIBIÇÕES EXPLÍCITAS
───────────────────────────────────────────────────────────────────────────────
✗ NÃO editar arquivos sem commit anterior
✗ NÃO criar documentação específica de versão (ex: DEPLOY_v1.9.0.md)
✗ NÃO duplicar informação em múltiplos arquivos
✗ NÃO fazer deploy manual (usar scripts)
✗ NÃO commitar sem testar em produção
═══════════════════════════════════════════════════════════════════════════════
INFRAESTRUTURA
═══════════════════════════════════════════════════════════════════════════════
───────────────────────────────────────────────────────────────────────────────
SERVIDOR DE PRODUÇÃO
───────────────────────────────────────────────────────────────────────────────
IP: 213.165.93.60
Porta SSH: 22
Usuário: root
Senha: Master9354
Acesso Windows (PowerShell):
plink -batch -pw Master9354 root@213.165.93.60 "comando"
Estrutura:
/var/www/webmoney/
├── backend/ # Laravel API
└── frontend/
└── dist/ # React build (Nginx root)
───────────────────────────────────────────────────────────────────────────────
DOMÍNIOS
───────────────────────────────────────────────────────────────────────────────
| Subdomínio | Função |
|------------|--------|
| webmoney.cnxifly.com | Aplicação principal |
| phpmyadmin.cnxifly.com | Banco de dados |
| webmail.cnxifly.com | Email |
| mail.cnxifly.com | PostfixAdmin |
───────────────────────────────────────────────────────────────────────────────
STACK TECNOLÓGICA
───────────────────────────────────────────────────────────────────────────────
| Camada | Tecnologia |
|--------|------------|
| Backend | Laravel 12 + PHP 8.4-FPM |
| Frontend | React 18 + Vite 7 + Bootstrap 5 |
| Banco | MariaDB 11.4 |
| Cache | Redis |
| Servidor | Nginx + SSL (Let's Encrypt) |
| Auth | Laravel Sanctum (Bearer Tokens) |
───────────────────────────────────────────────────────────────────────────────
AMBIENTE DE DESENVOLVIMENTO (WINDOWS)
───────────────────────────────────────────────────────────────────────────────
Requisitos:
- Windows 10/11
- PowerShell 5.1+
- Node.js 20+
- PuTTY (plink.exe, pscp.exe)
- VS Code
Ferramentas de conexão:
| Comando Linux | Equivalente Windows |
|---------------|---------------------|
| ssh user@host | plink -batch -pw SENHA user@host |
| scp file user@host:path | pscp -batch -pw SENHA file user@host:path |
| scp -r dir user@host:path | pscp -r -batch -pw SENHA dir user@host:path |
Flags importantes:
-batch : Não solicita interação (senhas, confirmações)
-pw : Fornece senha diretamente
═══════════════════════════════════════════════════════════════════════════════
SEGURANÇA
═══════════════════════════════════════════════════════════════════════════════
Implementado em v1.19.0:
| Recurso | Configuração |
|---------|--------------|
| Rate Limiting | Login: 5/min, Register: 10/hour |
| CORS | Restrito a webmoney.cnxifly.com |
| Token Expiration | 7 dias |
| Cookies | HttpOnly, Secure, SameSite=lax, Encrypt=true |
| Headers | X-XSS-Protection, X-Content-Type-Options, X-Frame-Options, CSP |
| Cookie Consent | Banner LGPD/GDPR |
═══════════════════════════════════════════════════════════════════════════════
ESTADO ATUAL
═══════════════════════════════════════════════════════════════════════════════
Versão: 1.27.2
Data: 13 de Dezembro de 2025
Status: Produção estável
Funcionalidades:
✅ Autenticação (login, registro, logout)
✅ Dashboard (gráficos, análises, widget overdue)
✅ Contas bancárias (CRUD, multi-moeda)
✅ Transações (agrupamento por semana, filtros, categorização em lote com seleção)
✅ Categorias (175 pré-configuradas, auto-classificação, keywords)
✅ Centros de custo
✅ Importação de extratos (XLSX, CSV, OFX, PDF)
✅ Detecção de duplicatas (auto-delete)
✅ Detecção de transferências
✅ Contas passivo (financiamentos)
✅ Transações recorrentes (templates, instâncias, conciliação)
✅ Multi-idioma (ES, PT-BR, EN) com detecção por país
✅ Tema dark
✅ Cookie consent (LGPD/GDPR)
✅ Segurança hardening
═══════════════════════════════════════════════════════════════════════════════
HISTÓRICO DE DIRETRIZES
═══════════════════════════════════════════════════════════════════════════════
| Versão | Data | Mudanças |
|--------|------|----------|
| v1.0 | 2025-12-07 | Criação inicial |
| v2.0 | 2025-12-08 | Adicionada REGRA #8 (ESTRUTURA_PROJETO) |
| v3.0 | 2025-12-10 | Simplificação, remoção de redundâncias |
| v4.0 | 2025-12-13 | Migração para Windows (PowerShell, PuTTY) |
Arquivos de diretrizes:
- .DIRETRIZES_DESENVOLVIMENTO (v1.0 - EXCLUÍDO)
- .DIRETRIZES_DESENVOLVIMENTO_v2 (v2.0 - arquivado)
- .DIRETRIZES_DESENVOLVIMENTO_v3 (v3.0 - arquivado)
- .DIRETRIZES_DESENVOLVIMENTO_v4 (v4.0 - ATIVO)
═══════════════════════════════════════════════════════════════════════════════
⚠️ LEMBRETE FINAL
═══════════════════════════════════════════════════════════════════════════════
ANTES de editar qualquer arquivo:
1. ✓ Último commit foi feito?
2. ✓ VERSION será incrementado?
3. ✓ CHANGELOG será atualizado?
4. ✓ Deploy será feito via script (.\deploy.ps1)?
5. ✓ Teste em produção será realizado?
Este documento é IMUTÁVEL. Qualquer mudança requer criar v5.0.
═══════════════════════════════════════════════════════════════════════════════

View File

@ -5,6 +5,71 @@ O formato segue [Keep a Changelog](https://keepachangelog.com/pt-BR/).
Este projeto adota [Versionamento Semântico](https://semver.org/pt-BR/).
## [1.42.0] - 2025-12-16
### Added
- **Navegação entre semanas no Calendário Mobile** - Implementada paginação completa
- Botões chevron esquerda/direita para navegar entre semanas
- Título dinâmico: "Esta Semana", "+1 semana", "-2 semanas", etc.
- Botão "Hoje" reseta para semana atual (weekOffset = 0)
- Calendário carrega dados automaticamente da semana selecionada
- Offset de semanas persistente durante navegação
### Fixed
- **Calendário não exibia em Mobile** - Corrigida lógica de renderização
- Problema: condição `day.isCurrentMonth` escondia dias em mobile
- Solução: `(isMobile || day.isCurrentMonth)` permite mostrar semana completa
- Adicionado grid CSS: `.calendar-grid` e `.calendar-grid-week` (7 colunas)
- Botão toggle (chevron) implementado no header mobile
- Badge mostra quantidade de transações do dia selecionado
- Tamanhos específicos: mobile (11px, 50px min-height) vs desktop (12px, 32px)
- Destaque visual do dia atual com border primary
### Changed
- **Widget Transações em Atraso - Comportamento Mobile** - Consistência com outros widgets
- Inicia colapsado em mobile (apenas header visível)
- Auto-expansão quando há transações vencidas
- Botão toggle chevron up/down no header
- Badge mostra total de itens vencidos
- Altura dinâmica: `height: auto` em mobile
- Body com `display: none` quando colapsado
### Improved
- **Consistência Mobile** - Todos os widgets do Dashboard seguem mesmo padrão:
- ✅ Calendário: colapso + navegação entre semanas
- ✅ Próximos 7 Dias: colapso + auto-expansão com dados
- ✅ Transações em Atraso: colapso + auto-expansão com dados
- UX unificada e previsível em dispositivos móveis
## [1.41.1] - 2025-12-16
### Fixed
- **Sincronização de altura em Desktop** - Widget "Próximos 7 Dias" agora sincroniza perfeitamente com o card central de transações
- Corrigido seletor para buscar `.col-lg-8 .col-lg-7 .card` (lista de transações do dia)
- Aumentado timeout de 100ms para 200ms para garantir renderização completa
- Altura idêntica entre os 3 widgets em desktop: calendário, transações do dia e próximos 7 dias
### Validated
- ✅ Mobile: Widgets colapsam corretamente, expandem apenas com dados
- ✅ Desktop: Altura sincronizada perfeitamente entre todos os widgets
## [1.41.0] - 2025-12-16
### Changed
- **WIDGETS COLAPSÁVEIS EM MOBILE** - Otimização do Dashboard para telas pequenas
- Calendário e Próximos 7 Dias iniciam colapsados em mobile (<768px)
- Expansão automática quando houver dados para exibir
- Botão toggle (chevron up/down) no header para expandir/colapsar manualmente
- Badge com contagem de itens no header quando há dados
- CalendarWidget: mostra quantidade de transações do dia selecionado
- UpcomingWidget: mostra quantidade total de transações pendentes
- Comportamento desktop mantido: sempre expandido
### Improved
- **UX Mobile** - Menos scroll necessário, informação visível apenas quando relevante
- **Performance** - Reduz altura inicial da página em mobile
- **Feedback visual** - Badges indicam quantidade de dados sem necessidade de expandir
## [1.40.0] - 2025-12-16
### Changed

View File

@ -1 +1 @@
1.40.0
1.42.0

View File

@ -20,6 +20,26 @@ const CalendarWidget = () => {
const [dayLoading, setDayLoading] = useState(false);
const [actionLoading, setActionLoading] = useState(null);
const [calendarHeight, setCalendarHeight] = useState(null);
// Detectar mobile para visualização semanal
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
const [isExpanded, setIsExpanded] = useState(false);
const [weekOffset, setWeekOffset] = useState(0); // Offset de semanas em mobile (0 = semana atual)
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth < 768);
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// Auto-expandir quando houver dados em mobile
useEffect(() => {
if (isMobile && dayData?.items?.length > 0 && !isExpanded) {
setIsExpanded(true);
}
}, [isMobile, dayData, isExpanded]);
// Estado do modal de conciliação
const [showReconcileModal, setShowReconcileModal] = useState(false);
@ -109,6 +129,18 @@ const CalendarWidget = () => {
const today = new Date();
setCurrentDate(new Date(today.getFullYear(), today.getMonth(), 1));
setSelectedDate(today.toISOString().split('T')[0]);
setWeekOffset(0); // Voltar para semana atual em mobile
};
// Navegação de semanas em mobile
const prevWeek = () => {
setWeekOffset(weekOffset - 1);
setSelectedDate(null);
};
const nextWeek = () => {
setWeekOffset(weekOffset + 1);
setSelectedDate(null);
};
// Obter dados de uma data específica
@ -122,6 +154,30 @@ const CalendarWidget = () => {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
// MOBILE: Visualização semanal
if (isMobile) {
const today = new Date();
const startOfWeek = new Date(today);
startOfWeek.setDate(today.getDate() - today.getDay() + (weekOffset * 7)); // Domingo + offset
const days = [];
for (let i = 0; i < 7; i++) {
const date = new Date(startOfWeek);
date.setDate(startOfWeek.getDate() + i);
const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
const todayStr = today.toISOString().split('T')[0];
days.push({
day: date.getDate(),
isCurrentMonth: date.getMonth() === today.getMonth(),
date: dateStr,
data: getDateData(dateStr),
isToday: dateStr === todayStr,
});
}
return days;
}
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
@ -273,34 +329,76 @@ const CalendarWidget = () => {
<h6 className="text-white mb-0 d-flex align-items-center">
<i className="bi bi-calendar3 me-2 text-primary"></i>
{t('dashboard.calendar')}
{isMobile && dayData?.items?.length > 0 && (
<span className="badge bg-primary ms-2" style={{ fontSize: '10px' }}>
{dayData.items.length}
</span>
)}
</h6>
<button
className="btn btn-outline-primary btn-sm"
onClick={goToToday}
style={{ fontSize: '11px' }}
>
{t('common.today')}
</button>
</div>
<div className="card-body pt-0">
{/* Navegação do mês */}
<div className="d-flex justify-content-between align-items-center mb-3">
<button
className="btn btn-link text-slate-400 p-0"
onClick={prevMonth}
>
<i className="bi bi-chevron-left"></i>
</button>
<span className="text-white fw-medium">
{monthNames[currentDate.getMonth()]} {currentDate.getFullYear()}
</span>
<button
className="btn btn-link text-slate-400 p-0"
onClick={nextMonth}
>
<i className="bi bi-chevron-right"></i>
</button>
<div className="d-flex gap-2">
{isMobile && (
<button
className="btn btn-link text-primary p-0"
onClick={() => setIsExpanded(!isExpanded)}
>
<i className={`bi bi-chevron-${isExpanded ? 'up' : 'down'}`}></i>
</button>
)}
{!isMobile && (
<button
className="btn btn-outline-primary btn-sm"
onClick={goToToday}
style={{ fontSize: '11px' }}
>
{t('common.today')}
</button>
)}
</div>
</div>
<div className="card-body pt-0" style={{
display: isMobile && !isExpanded ? 'none' : 'block'
}}>
{/* Navegação do mês/semana */}
{!isMobile && (
<div className="d-flex justify-content-between align-items-center mb-3">
<button
className="btn btn-link text-slate-400 p-0"
onClick={prevMonth}
>
<i className="bi bi-chevron-left"></i>
</button>
<span className="text-white fw-medium">
{monthNames[currentDate.getMonth()]} {currentDate.getFullYear()}
</span>
<button
className="btn btn-link text-slate-400 p-0"
onClick={nextMonth}
>
<i className="bi bi-chevron-right"></i>
</button>
</div>
)}
{isMobile && (
<div className="d-flex justify-content-between align-items-center mb-3">
<button
className="btn btn-link text-slate-400 p-0"
onClick={prevWeek}
>
<i className="bi bi-chevron-left"></i>
</button>
<span className="text-white fw-medium">
{weekOffset === 0 ? (t('dashboard.thisWeek') || 'Esta Semana') :
weekOffset > 0 ? `+${weekOffset} ${weekOffset === 1 ? 'semana' : 'semanas'}` :
`${weekOffset} ${weekOffset === -1 ? 'semana' : 'semanas'}`}
</span>
<button
className="btn btn-link text-slate-400 p-0"
onClick={nextWeek}
>
<i className="bi bi-chevron-right"></i>
</button>
</div>
)}
{loading ? (
<div className="text-center py-4">
@ -320,19 +418,21 @@ const CalendarWidget = () => {
</div>
{/* Dias */}
<div className="calendar-grid">
<div className={isMobile ? "calendar-grid-week" : "calendar-grid"}>
{days.map((day, index) => (
<div key={index} className="calendar-day">
{day.isCurrentMonth ? (
{(isMobile || day.isCurrentMonth) ? (
<button
className={`btn w-100 h-100 p-0 d-flex flex-column align-items-center justify-content-center rounded-2
${day.date === selectedDate ? 'btn-primary' : 'btn-link'}
${day.date === today && day.date !== selectedDate ? 'border border-primary' : ''}
${day.isToday && day.date !== selectedDate ? 'border border-primary' : ''}
${!day.isCurrentMonth && !isMobile ? 'opacity-50' : ''}
`}
style={{
fontSize: '12px',
fontSize: isMobile ? '11px' : '12px',
background: day.date === selectedDate ? undefined : 'transparent',
color: day.date === selectedDate ? 'white' : day.data ? '#fff' : '#64748b',
minHeight: isMobile ? '50px' : '32px',
}}
onClick={() => setSelectedDate(day.date)}
>

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { dashboardService } from '../../services/api';
@ -8,10 +8,29 @@ const OverdueWidget = () => {
const { t } = useTranslation();
const { currency } = useFormatters();
const navigate = useNavigate();
const cardRef = useRef(null);
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [expandedRange, setExpandedRange] = useState(null);
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
const [isExpanded, setIsExpanded] = useState(false);
// Detectar mudanças de tamanho da tela
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth < 768);
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// Auto-expandir quando houver dados em mobile
useEffect(() => {
if (isMobile && data?.items?.length > 0 && !isExpanded) {
setIsExpanded(true);
}
}, [isMobile, data, isExpanded]);
const loadData = useCallback(async () => {
setLoading(true);
@ -84,7 +103,14 @@ const OverdueWidget = () => {
};
return (
<div className="card border-0" style={{ background: '#0f172a' }}>
<div
ref={cardRef}
className="card border-0"
style={{
background: '#0f172a',
height: isMobile ? 'auto' : undefined
}}
>
<div className="card-header border-0 d-flex justify-content-between align-items-center py-2"
style={{ background: 'transparent', borderBottom: '1px solid rgba(239, 68, 68, 0.3)' }}>
<h6 className="text-white mb-0 d-flex align-items-center">
@ -94,16 +120,28 @@ const OverdueWidget = () => {
<span className="badge bg-danger ms-2">{data.summary.total_items}</span>
)}
</h6>
<button
className="btn btn-sm btn-outline-secondary border-0"
onClick={loadData}
disabled={loading}
>
<i className={`bi bi-arrow-clockwise ${loading ? 'spin' : ''}`}></i>
</button>
<div className="d-flex gap-2">
{isMobile && (
<button
className="btn btn-link text-primary p-0"
onClick={() => setIsExpanded(!isExpanded)}
>
<i className={`bi bi-chevron-${isExpanded ? 'up' : 'down'}`}></i>
</button>
)}
<button
className="btn btn-sm btn-outline-secondary border-0"
onClick={loadData}
disabled={loading}
>
<i className={`bi bi-arrow-clockwise ${loading ? 'spin' : ''}`}></i>
</button>
</div>
</div>
<div className="card-body py-3">
<div className="card-body py-3" style={{
display: isMobile && !isExpanded ? 'none' : 'block'
}}>
{loading ? (
<div className="text-center py-4">
<div className="spinner-border spinner-border-sm text-danger" role="status">

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { dashboardService } from '../../services/api';
import useFormatters from '../../hooks/useFormatters';
@ -6,9 +6,67 @@ import useFormatters from '../../hooks/useFormatters';
const UpcomingWidget = () => {
const { t } = useTranslation();
const { currency } = useFormatters();
const cardRef = useRef(null);
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
const [isExpanded, setIsExpanded] = useState(false);
const [calendarHeight, setCalendarHeight] = useState(null);
// Detectar mudanças de tamanho da tela
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth < 768);
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// Auto-expandir quando houver dados em mobile
useEffect(() => {
const hasItems = data?.by_date?.some(day => day.items?.length > 0);
if (isMobile && hasItems && !isExpanded) {
setIsExpanded(true);
}
}, [isMobile, data, isExpanded]);
// Sincronizar altura com CalendarWidget em desktop
useEffect(() => {
if (isMobile) {
setCalendarHeight(null);
return;
}
const updateHeight = () => {
const row = cardRef.current?.closest('.row.g-4.mb-4');
if (row) {
// Buscar o card da lista de transações (col-lg-7) dentro do CalendarWidget
const transactionsCard = row.querySelector('.col-lg-8 .col-lg-7 .card');
if (transactionsCard) {
setCalendarHeight(transactionsCard.offsetHeight);
}
}
};
const timer = setTimeout(updateHeight, 200);
window.addEventListener('resize', updateHeight);
const observer = new ResizeObserver(updateHeight);
const row = cardRef.current?.closest('.row.g-4.mb-4');
if (row) {
const transactionsCard = row.querySelector('.col-lg-8 .col-lg-7 .card');
if (transactionsCard) {
observer.observe(transactionsCard);
}
}
return () => {
clearTimeout(timer);
window.removeEventListener('resize', updateHeight);
observer.disconnect();
};
}, [isMobile, loading, data]);
const loadData = useCallback(async () => {
setLoading(true);
@ -33,12 +91,8 @@ const UpcomingWidget = () => {
};
const getTypeIcon = (item) => {
if (item.type === 'recurring') {
return 'bi-arrow-repeat';
}
if (item.is_transfer) {
return 'bi-arrow-left-right';
}
if (item.type === 'recurring') return 'bi-arrow-repeat';
if (item.is_transfer) return 'bi-arrow-left-right';
return item.transaction_type === 'credit' ? 'bi-arrow-down-circle' : 'bi-arrow-up-circle';
};
@ -49,23 +103,49 @@ const UpcomingWidget = () => {
};
return (
<div className="card border-0 d-flex flex-column" style={{ background: '#0f172a', height: '320px' }}>
<div
ref={cardRef}
className="card border-0 d-flex flex-column"
style={{
background: '#0f172a',
height: isMobile ? 'auto' : (calendarHeight ? `${calendarHeight}px` : 'auto')
}}
>
<div className="card-header border-0 d-flex justify-content-between align-items-center py-2 flex-shrink-0"
style={{ background: 'transparent', borderBottom: '1px solid rgba(59, 130, 246, 0.2)' }}>
<h6 className="text-white mb-0 d-flex align-items-center">
<i className="bi bi-clock-history me-2 text-primary"></i>
{t('dashboard.upcomingTransactions')}
{isMobile && data?.by_date?.some(day => day.items?.length > 0) && (
<span className="badge bg-primary ms-2" style={{ fontSize: '10px' }}>
{data.by_date.reduce((sum, day) => sum + day.items.length, 0)}
</span>
)}
</h6>
<button
className="btn btn-sm btn-outline-secondary border-0"
onClick={loadData}
disabled={loading}
>
<i className={`bi bi-arrow-clockwise ${loading ? 'spin' : ''}`}></i>
</button>
<div className="d-flex gap-2">
{isMobile && (
<button
className="btn btn-link text-primary p-0"
onClick={() => setIsExpanded(!isExpanded)}
>
<i className={`bi bi-chevron-${isExpanded ? 'up' : 'down'}`}></i>
</button>
)}
<button
className="btn btn-sm btn-outline-secondary border-0"
onClick={loadData}
disabled={loading}
>
<i className={`bi bi-arrow-clockwise ${loading ? 'spin' : ''}`}></i>
</button>
</div>
</div>
<div className="card-body py-2 flex-grow-1" style={{ overflowY: 'auto', minHeight: 0 }}>
<div className="card-body py-2 flex-grow-1" style={{
overflowY: 'auto',
minHeight: 0,
display: isMobile && !isExpanded ? 'none' : 'block'
}}>
{loading ? (
<div className="text-center py-4">
<div className="spinner-border spinner-border-sm text-primary" role="status">
@ -81,7 +161,6 @@ const UpcomingWidget = () => {
<>
{data.by_date.map((day) => (
<div key={day.date} className="mb-3">
{/* Header do dia */}
<div className="d-flex align-items-center justify-content-between mb-2">
<div className="d-flex align-items-center gap-2">
<span
@ -97,7 +176,6 @@ const UpcomingWidget = () => {
</small>
</div>
{/* Items do dia */}
{day.items.map((item) => (
<div
key={`${item.type}-${item.id}`}
@ -107,7 +185,6 @@ const UpcomingWidget = () => {
borderLeft: `3px solid ${getTypeColor(item)}`,
}}
>
{/* Ícone */}
<div
className="rounded-circle d-flex align-items-center justify-content-center flex-shrink-0"
style={{
@ -120,7 +197,6 @@ const UpcomingWidget = () => {
<i className={`bi ${getTypeIcon(item)}`} style={{ fontSize: '12px' }}></i>
</div>
{/* Info */}
<div className="flex-grow-1 min-width-0">
<div className="text-white small text-truncate" title={item.description}>
{item.description}
@ -137,7 +213,6 @@ const UpcomingWidget = () => {
</div>
</div>
{/* Valor */}
<div className={`fw-bold small text-end ${
item.transaction_type === 'credit' ? 'text-success' : 'text-danger'
}`}>
@ -152,7 +227,6 @@ const UpcomingWidget = () => {
)}
</div>
{/* Footer com resumo */}
{data?.summary && !loading && data.by_date?.length > 0 && (
<div className="card-footer border-0 py-2" style={{ background: 'rgba(30, 41, 59, 0.5)' }}>
<div className="row g-2 text-center">

View File

@ -130,6 +130,7 @@
"upcomingTransactions": "Next 7 Days",
"noUpcomingTransactions": "No pending transactions",
"today": "Today",
"thisWeek": "This Week",
"tomorrow": "Tomorrow",
"daysAhead": "days",
"pendingRecurring": "Pending Recurring",

View File

@ -130,6 +130,7 @@
"upcomingTransactions": "Próximos 7 Días",
"noUpcomingTransactions": "No hay transacciones pendientes",
"today": "Hoy",
"thisWeek": "Esta Semana",
"tomorrow": "Mañana",
"daysAhead": "días",
"pendingRecurring": "Recurrentes Pendientes",

View File

@ -131,6 +131,7 @@
"upcomingTransactions": "Próximos 7 Dias",
"noUpcomingTransactions": "Nenhuma transação pendente",
"today": "Hoje",
"thisWeek": "Esta Semana",
"tomorrow": "Amanhã",
"daysAhead": "dias",
"pendingRecurring": "Recorrentes Pendentes",

View File

@ -2511,25 +2511,25 @@ input[type="color"]::-webkit-color-swatch {
🍎 iOS / iPhone Specific Optimizations
============================================== */
/* Touch targets mínimos de 44x44px (Apple HIG) */
.btn,
.btn-sm,
.btn-lg,
button,
a.btn,
.form-check-input,
.form-switch .form-check-input,
.dropdown-toggle,
.nav-link,
.sidebar-link,
.sidebar-group-toggle {
min-height: 44px !important;
min-width: 44px !important;
touch-action: manipulation; /* Desabilita zoom duplo-toque */
}
/* Cards e containers em telas pequenas */
@media (max-width: 576px) {
/* Touch targets mínimos de 44x44px (Apple HIG) - APENAS EM MOBILE */
.btn,
.btn-sm,
.btn-lg,
button,
a.btn,
.form-check-input,
.form-switch .form-check-input,
.dropdown-toggle,
.nav-link,
.sidebar-link,
.sidebar-group-toggle {
min-height: 44px !important;
min-width: 44px !important;
touch-action: manipulation; /* Desabilita zoom duplo-toque */
}
.card {
margin-bottom: 0.75rem !important;
border-radius: 0.75rem !important;
@ -2800,3 +2800,25 @@ a,
}
}
/* Calendário Grid */
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
}
.calendar-grid-week {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
}
.calendar-day {
min-height: 32px;
}
@media (max-width: 768px) {
.calendar-day {
min-height: 50px;
}
}