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/).
|
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
|
## [1.40.0] - 2025-12-16
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@ -20,6 +20,26 @@ const CalendarWidget = () => {
|
|||||||
const [dayLoading, setDayLoading] = useState(false);
|
const [dayLoading, setDayLoading] = useState(false);
|
||||||
const [actionLoading, setActionLoading] = useState(null);
|
const [actionLoading, setActionLoading] = useState(null);
|
||||||
const [calendarHeight, setCalendarHeight] = 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
|
// Estado do modal de conciliação
|
||||||
const [showReconcileModal, setShowReconcileModal] = useState(false);
|
const [showReconcileModal, setShowReconcileModal] = useState(false);
|
||||||
@ -109,6 +129,18 @@ const CalendarWidget = () => {
|
|||||||
const today = new Date();
|
const today = new Date();
|
||||||
setCurrentDate(new Date(today.getFullYear(), today.getMonth(), 1));
|
setCurrentDate(new Date(today.getFullYear(), today.getMonth(), 1));
|
||||||
setSelectedDate(today.toISOString().split('T')[0]);
|
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
|
// Obter dados de uma data específica
|
||||||
@ -122,6 +154,30 @@ const CalendarWidget = () => {
|
|||||||
const year = currentDate.getFullYear();
|
const year = currentDate.getFullYear();
|
||||||
const month = currentDate.getMonth();
|
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 firstDay = new Date(year, month, 1);
|
||||||
const lastDay = new Date(year, month + 1, 0);
|
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">
|
<h6 className="text-white mb-0 d-flex align-items-center">
|
||||||
<i className="bi bi-calendar3 me-2 text-primary"></i>
|
<i className="bi bi-calendar3 me-2 text-primary"></i>
|
||||||
{t('dashboard.calendar')}
|
{t('dashboard.calendar')}
|
||||||
|
{isMobile && dayData?.items?.length > 0 && (
|
||||||
|
<span className="badge bg-primary ms-2" style={{ fontSize: '10px' }}>
|
||||||
|
{dayData.items.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</h6>
|
</h6>
|
||||||
<button
|
<div className="d-flex gap-2">
|
||||||
className="btn btn-outline-primary btn-sm"
|
{isMobile && (
|
||||||
onClick={goToToday}
|
<button
|
||||||
style={{ fontSize: '11px' }}
|
className="btn btn-link text-primary p-0"
|
||||||
>
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
{t('common.today')}
|
>
|
||||||
</button>
|
<i className={`bi bi-chevron-${isExpanded ? 'up' : 'down'}`}></i>
|
||||||
</div>
|
</button>
|
||||||
<div className="card-body pt-0">
|
)}
|
||||||
{/* Navegação do mês */}
|
{!isMobile && (
|
||||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
<button
|
||||||
<button
|
className="btn btn-outline-primary btn-sm"
|
||||||
className="btn btn-link text-slate-400 p-0"
|
onClick={goToToday}
|
||||||
onClick={prevMonth}
|
style={{ fontSize: '11px' }}
|
||||||
>
|
>
|
||||||
<i className="bi bi-chevron-left"></i>
|
{t('common.today')}
|
||||||
</button>
|
</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>
|
</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 ? (
|
{loading ? (
|
||||||
<div className="text-center py-4">
|
<div className="text-center py-4">
|
||||||
@ -320,19 +418,21 @@ const CalendarWidget = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Dias */}
|
{/* Dias */}
|
||||||
<div className="calendar-grid">
|
<div className={isMobile ? "calendar-grid-week" : "calendar-grid"}>
|
||||||
{days.map((day, index) => (
|
{days.map((day, index) => (
|
||||||
<div key={index} className="calendar-day">
|
<div key={index} className="calendar-day">
|
||||||
{day.isCurrentMonth ? (
|
{(isMobile || day.isCurrentMonth) ? (
|
||||||
<button
|
<button
|
||||||
className={`btn w-100 h-100 p-0 d-flex flex-column align-items-center justify-content-center rounded-2
|
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 === 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={{
|
style={{
|
||||||
fontSize: '12px',
|
fontSize: isMobile ? '11px' : '12px',
|
||||||
background: day.date === selectedDate ? undefined : 'transparent',
|
background: day.date === selectedDate ? undefined : 'transparent',
|
||||||
color: day.date === selectedDate ? 'white' : day.data ? '#fff' : '#64748b',
|
color: day.date === selectedDate ? 'white' : day.data ? '#fff' : '#64748b',
|
||||||
|
minHeight: isMobile ? '50px' : '32px',
|
||||||
}}
|
}}
|
||||||
onClick={() => setSelectedDate(day.date)}
|
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 { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { dashboardService } from '../../services/api';
|
import { dashboardService } from '../../services/api';
|
||||||
@ -8,10 +8,29 @@ const OverdueWidget = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { currency } = useFormatters();
|
const { currency } = useFormatters();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const cardRef = useRef(null);
|
||||||
|
|
||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [expandedRange, setExpandedRange] = useState(null);
|
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 () => {
|
const loadData = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -84,7 +103,14 @@ const OverdueWidget = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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"
|
<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)' }}>
|
style={{ background: 'transparent', borderBottom: '1px solid rgba(239, 68, 68, 0.3)' }}>
|
||||||
<h6 className="text-white mb-0 d-flex align-items-center">
|
<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>
|
<span className="badge bg-danger ms-2">{data.summary.total_items}</span>
|
||||||
)}
|
)}
|
||||||
</h6>
|
</h6>
|
||||||
<button
|
<div className="d-flex gap-2">
|
||||||
className="btn btn-sm btn-outline-secondary border-0"
|
{isMobile && (
|
||||||
onClick={loadData}
|
<button
|
||||||
disabled={loading}
|
className="btn btn-link text-primary p-0"
|
||||||
>
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
<i className={`bi bi-arrow-clockwise ${loading ? 'spin' : ''}`}></i>
|
>
|
||||||
</button>
|
<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>
|
||||||
|
|
||||||
<div className="card-body py-3">
|
<div className="card-body py-3" style={{
|
||||||
|
display: isMobile && !isExpanded ? 'none' : 'block'
|
||||||
|
}}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="text-center py-4">
|
<div className="text-center py-4">
|
||||||
<div className="spinner-border spinner-border-sm text-danger" role="status">
|
<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 { useTranslation } from 'react-i18next';
|
||||||
import { dashboardService } from '../../services/api';
|
import { dashboardService } from '../../services/api';
|
||||||
import useFormatters from '../../hooks/useFormatters';
|
import useFormatters from '../../hooks/useFormatters';
|
||||||
@ -6,9 +6,67 @@ import useFormatters from '../../hooks/useFormatters';
|
|||||||
const UpcomingWidget = () => {
|
const UpcomingWidget = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { currency } = useFormatters();
|
const { currency } = useFormatters();
|
||||||
|
const cardRef = useRef(null);
|
||||||
|
|
||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
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 () => {
|
const loadData = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -33,12 +91,8 @@ const UpcomingWidget = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getTypeIcon = (item) => {
|
const getTypeIcon = (item) => {
|
||||||
if (item.type === 'recurring') {
|
if (item.type === 'recurring') return 'bi-arrow-repeat';
|
||||||
return 'bi-arrow-repeat';
|
if (item.is_transfer) return 'bi-arrow-left-right';
|
||||||
}
|
|
||||||
if (item.is_transfer) {
|
|
||||||
return 'bi-arrow-left-right';
|
|
||||||
}
|
|
||||||
return item.transaction_type === 'credit' ? 'bi-arrow-down-circle' : 'bi-arrow-up-circle';
|
return item.transaction_type === 'credit' ? 'bi-arrow-down-circle' : 'bi-arrow-up-circle';
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -49,23 +103,49 @@ const UpcomingWidget = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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"
|
<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)' }}>
|
style={{ background: 'transparent', borderBottom: '1px solid rgba(59, 130, 246, 0.2)' }}>
|
||||||
<h6 className="text-white mb-0 d-flex align-items-center">
|
<h6 className="text-white mb-0 d-flex align-items-center">
|
||||||
<i className="bi bi-clock-history me-2 text-primary"></i>
|
<i className="bi bi-clock-history me-2 text-primary"></i>
|
||||||
{t('dashboard.upcomingTransactions')}
|
{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>
|
</h6>
|
||||||
<button
|
<div className="d-flex gap-2">
|
||||||
className="btn btn-sm btn-outline-secondary border-0"
|
{isMobile && (
|
||||||
onClick={loadData}
|
<button
|
||||||
disabled={loading}
|
className="btn btn-link text-primary p-0"
|
||||||
>
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
<i className={`bi bi-arrow-clockwise ${loading ? 'spin' : ''}`}></i>
|
>
|
||||||
</button>
|
<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>
|
||||||
|
|
||||||
<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 ? (
|
{loading ? (
|
||||||
<div className="text-center py-4">
|
<div className="text-center py-4">
|
||||||
<div className="spinner-border spinner-border-sm text-primary" role="status">
|
<div className="spinner-border spinner-border-sm text-primary" role="status">
|
||||||
@ -81,7 +161,6 @@ const UpcomingWidget = () => {
|
|||||||
<>
|
<>
|
||||||
{data.by_date.map((day) => (
|
{data.by_date.map((day) => (
|
||||||
<div key={day.date} className="mb-3">
|
<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 justify-content-between mb-2">
|
||||||
<div className="d-flex align-items-center gap-2">
|
<div className="d-flex align-items-center gap-2">
|
||||||
<span
|
<span
|
||||||
@ -97,7 +176,6 @@ const UpcomingWidget = () => {
|
|||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Items do dia */}
|
|
||||||
{day.items.map((item) => (
|
{day.items.map((item) => (
|
||||||
<div
|
<div
|
||||||
key={`${item.type}-${item.id}`}
|
key={`${item.type}-${item.id}`}
|
||||||
@ -107,7 +185,6 @@ const UpcomingWidget = () => {
|
|||||||
borderLeft: `3px solid ${getTypeColor(item)}`,
|
borderLeft: `3px solid ${getTypeColor(item)}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Ícone */}
|
|
||||||
<div
|
<div
|
||||||
className="rounded-circle d-flex align-items-center justify-content-center flex-shrink-0"
|
className="rounded-circle d-flex align-items-center justify-content-center flex-shrink-0"
|
||||||
style={{
|
style={{
|
||||||
@ -120,7 +197,6 @@ const UpcomingWidget = () => {
|
|||||||
<i className={`bi ${getTypeIcon(item)}`} style={{ fontSize: '12px' }}></i>
|
<i className={`bi ${getTypeIcon(item)}`} style={{ fontSize: '12px' }}></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Info */}
|
|
||||||
<div className="flex-grow-1 min-width-0">
|
<div className="flex-grow-1 min-width-0">
|
||||||
<div className="text-white small text-truncate" title={item.description}>
|
<div className="text-white small text-truncate" title={item.description}>
|
||||||
{item.description}
|
{item.description}
|
||||||
@ -137,7 +213,6 @@ const UpcomingWidget = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Valor */}
|
|
||||||
<div className={`fw-bold small text-end ${
|
<div className={`fw-bold small text-end ${
|
||||||
item.transaction_type === 'credit' ? 'text-success' : 'text-danger'
|
item.transaction_type === 'credit' ? 'text-success' : 'text-danger'
|
||||||
}`}>
|
}`}>
|
||||||
@ -152,7 +227,6 @@ const UpcomingWidget = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer com resumo */}
|
|
||||||
{data?.summary && !loading && data.by_date?.length > 0 && (
|
{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="card-footer border-0 py-2" style={{ background: 'rgba(30, 41, 59, 0.5)' }}>
|
||||||
<div className="row g-2 text-center">
|
<div className="row g-2 text-center">
|
||||||
|
|||||||
@ -130,6 +130,7 @@
|
|||||||
"upcomingTransactions": "Next 7 Days",
|
"upcomingTransactions": "Next 7 Days",
|
||||||
"noUpcomingTransactions": "No pending transactions",
|
"noUpcomingTransactions": "No pending transactions",
|
||||||
"today": "Today",
|
"today": "Today",
|
||||||
|
"thisWeek": "This Week",
|
||||||
"tomorrow": "Tomorrow",
|
"tomorrow": "Tomorrow",
|
||||||
"daysAhead": "days",
|
"daysAhead": "days",
|
||||||
"pendingRecurring": "Pending Recurring",
|
"pendingRecurring": "Pending Recurring",
|
||||||
|
|||||||
@ -130,6 +130,7 @@
|
|||||||
"upcomingTransactions": "Próximos 7 Días",
|
"upcomingTransactions": "Próximos 7 Días",
|
||||||
"noUpcomingTransactions": "No hay transacciones pendientes",
|
"noUpcomingTransactions": "No hay transacciones pendientes",
|
||||||
"today": "Hoy",
|
"today": "Hoy",
|
||||||
|
"thisWeek": "Esta Semana",
|
||||||
"tomorrow": "Mañana",
|
"tomorrow": "Mañana",
|
||||||
"daysAhead": "días",
|
"daysAhead": "días",
|
||||||
"pendingRecurring": "Recurrentes Pendientes",
|
"pendingRecurring": "Recurrentes Pendientes",
|
||||||
|
|||||||
@ -131,6 +131,7 @@
|
|||||||
"upcomingTransactions": "Próximos 7 Dias",
|
"upcomingTransactions": "Próximos 7 Dias",
|
||||||
"noUpcomingTransactions": "Nenhuma transação pendente",
|
"noUpcomingTransactions": "Nenhuma transação pendente",
|
||||||
"today": "Hoje",
|
"today": "Hoje",
|
||||||
|
"thisWeek": "Esta Semana",
|
||||||
"tomorrow": "Amanhã",
|
"tomorrow": "Amanhã",
|
||||||
"daysAhead": "dias",
|
"daysAhead": "dias",
|
||||||
"pendingRecurring": "Recorrentes Pendentes",
|
"pendingRecurring": "Recorrentes Pendentes",
|
||||||
|
|||||||
@ -2511,25 +2511,25 @@ input[type="color"]::-webkit-color-swatch {
|
|||||||
🍎 iOS / iPhone Specific Optimizations
|
🍎 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 */
|
/* Cards e containers em telas pequenas */
|
||||||
@media (max-width: 576px) {
|
@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 {
|
.card {
|
||||||
margin-bottom: 0.75rem !important;
|
margin-bottom: 0.75rem !important;
|
||||||
border-radius: 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