v1.42.0 - Mobile UX: Navegação entre semanas no calendário + Widgets colapsáveis consistentes
This commit is contained in:
parent
3ba4bed1c4
commit
1186faca3c
@ -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.
|
||||
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
65
CHANGELOG.md
65
CHANGELOG.md
@ -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
|
||||
|
||||
@ -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)}
|
||||
>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -130,6 +130,7 @@
|
||||
"upcomingTransactions": "Next 7 Days",
|
||||
"noUpcomingTransactions": "No pending transactions",
|
||||
"today": "Today",
|
||||
"thisWeek": "This Week",
|
||||
"tomorrow": "Tomorrow",
|
||||
"daysAhead": "days",
|
||||
"pendingRecurring": "Pending Recurring",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user