Update dependencies and enhance Dashboard and Landing pages with new features and UI improvements
This commit is contained in:
303
web/GUIA_COMPLETO_APACHE.md
Normal file
303
web/GUIA_COMPLETO_APACHE.md
Normal file
@@ -0,0 +1,303 @@
|
||||
# 🚀 Guia Completo - Passo a Passo Apache
|
||||
|
||||
Guia super simples com comandos prontos para copiar e colar.
|
||||
|
||||
---
|
||||
|
||||
## 📍 ONDE ESTÁ VOCÊ AGORA?
|
||||
|
||||
- ✅ **Passos 1-2:** No seu **COMPUTADOR MAC** (onde está o código)
|
||||
- ✅ **Passos 3-10:** No **SERVIDOR** (via SSH)
|
||||
|
||||
---
|
||||
|
||||
## ✅ PASSO 1: Criar o Build (NO SEU MAC)
|
||||
|
||||
**1.1 - Abra o Terminal no seu Mac**
|
||||
|
||||
**1.2 - Copie e cole este comando:**
|
||||
|
||||
```bash
|
||||
cd /Users/230417/SmartAgendaMobile/web
|
||||
```
|
||||
|
||||
**1.3 - Pressione Enter**
|
||||
|
||||
**1.4 - Copie e cole este comando:**
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
**1.5 - Pressione Enter e aguarde (1-2 minutos)**
|
||||
|
||||
**1.6 - Verifique se funcionou (deve aparecer "✓ built"):**
|
||||
|
||||
```bash
|
||||
ls -la dist/
|
||||
```
|
||||
|
||||
**Deve aparecer:** `index.html` e uma pasta `assets/`
|
||||
|
||||
---
|
||||
|
||||
## ✅ PASSO 2: Verificar Build (OPCIONAL - NO SEU MAC)
|
||||
|
||||
**2.1 - Ainda no terminal do Mac, copie e cole:**
|
||||
|
||||
```bash
|
||||
npm run preview
|
||||
```
|
||||
|
||||
**2.2 - Abra o navegador e acesse:** `http://localhost:4173`
|
||||
|
||||
**2.3 - Se estiver funcionando, volte ao terminal e pressione:** `Ctrl+C`
|
||||
|
||||
---
|
||||
|
||||
## ✅ PASSO 3: Conectar ao Servidor
|
||||
|
||||
**3.1 - No terminal do Mac, copie e cole (ajuste o IP/domínio):**
|
||||
|
||||
```bash
|
||||
ssh santospap@SEU-IP-OU-DOMINIO
|
||||
```
|
||||
|
||||
**Exemplo:** `ssh santospap@192.168.1.100` ou `ssh santospap@meuservidor.com`
|
||||
|
||||
**3.2 - Digite a senha quando pedir**
|
||||
|
||||
**3.3 - Agora você está no servidor!**
|
||||
|
||||
---
|
||||
|
||||
## ✅ PASSO 4: Criar Pasta no Servidor
|
||||
|
||||
**4.1 - No servidor (via SSH), copie e cole:**
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /var/www/html/smart-agenda
|
||||
```
|
||||
|
||||
**4.2 - Pressione Enter e digite a senha se pedir**
|
||||
|
||||
---
|
||||
|
||||
## ✅ PASSO 5: Copiar Arquivos (DO SEU MAC PARA O SERVIDOR)
|
||||
|
||||
**5.1 - Abra um NOVO terminal no seu Mac** (não feche o SSH)
|
||||
|
||||
**5.2 - Copie e cole este comando (ajuste o IP/domínio):**
|
||||
|
||||
```bash
|
||||
scp -r /Users/230417/SmartAgendaMobile/web/dist/* santospap@SEU-IP-OU-DOMINIO:/var/www/html/smart-agenda/
|
||||
```
|
||||
|
||||
**Exemplo:** `scp -r /Users/230417/SmartAgendaMobile/web/dist/* santospap@192.168.1.100:/var/www/html/smart-agenda/`
|
||||
|
||||
**5.3 - Digite a senha quando pedir**
|
||||
|
||||
**5.4 - Aguarde os arquivos serem copiados**
|
||||
|
||||
**5.5 - Verifique se os arquivos foram copiados (volte ao terminal SSH do servidor):**
|
||||
|
||||
```bash
|
||||
ls -la /var/www/html/smart-agenda/
|
||||
```
|
||||
|
||||
**Deve aparecer:** `index.html` e pasta `assets/`
|
||||
|
||||
---
|
||||
|
||||
## ✅ PASSO 6: Criar Arquivo .htaccess (NO SERVIDOR)
|
||||
|
||||
**6.1 - No servidor (via SSH), copie e cole:**
|
||||
|
||||
```bash
|
||||
sudo nano /var/www/html/smart-agenda/.htaccess
|
||||
```
|
||||
|
||||
**6.2 - Pressione Enter**
|
||||
|
||||
**6.3 - Cole este conteúdo (Ctrl+V ou botão direito > Paste):**
|
||||
|
||||
```apache
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule . /index.html [L]
|
||||
</IfModule>
|
||||
```
|
||||
|
||||
**6.4 - Para salvar:**
|
||||
- Pressione `Ctrl+O`
|
||||
- Pressione `Enter`
|
||||
- Pressione `Ctrl+X`
|
||||
|
||||
---
|
||||
|
||||
## ✅ PASSO 7: Configurar Permissões (NO SERVIDOR)
|
||||
|
||||
**7.1 - No servidor, copie e cole:**
|
||||
|
||||
```bash
|
||||
sudo chown -R www-data:www-data /var/www/html/smart-agenda
|
||||
```
|
||||
|
||||
**7.2 - Pressione Enter**
|
||||
|
||||
**7.3 - Copie e cole:**
|
||||
|
||||
```bash
|
||||
sudo chmod -R 755 /var/www/html/smart-agenda
|
||||
```
|
||||
|
||||
**7.4 - Pressione Enter**
|
||||
|
||||
**Nota:** Se usar CentOS/RHEL, use `apache:apache` em vez de `www-data:www-data`
|
||||
|
||||
---
|
||||
|
||||
## ✅ PASSO 8: Habilitar Módulo Rewrite (NO SERVIDOR)
|
||||
|
||||
**8.1 - Verifique qual sistema está usando:**
|
||||
|
||||
```bash
|
||||
cat /etc/os-release
|
||||
```
|
||||
|
||||
**8.2a - Se for Ubuntu/Debian, copie e cole:**
|
||||
|
||||
```bash
|
||||
sudo a2enmod rewrite
|
||||
sudo systemctl restart apache2
|
||||
```
|
||||
|
||||
**8.2b - Se for CentOS/RHEL, copie e cole:**
|
||||
|
||||
```bash
|
||||
sudo systemctl restart httpd
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ PASSO 9: Verificar se Está Funcionando
|
||||
|
||||
**9.1 - No navegador do seu computador, acesse:**
|
||||
|
||||
```
|
||||
http://SEU-IP-OU-DOMINIO/smart-agenda/
|
||||
```
|
||||
|
||||
**Exemplo:** `http://192.168.1.100/smart-agenda/`
|
||||
|
||||
**9.2 - Deve aparecer a aplicação Smart Agenda!**
|
||||
|
||||
**9.3 - Teste as rotas:**
|
||||
- `http://SEU-IP/smart-agenda/login`
|
||||
- `http://SEU-IP/smart-agenda/explorar`
|
||||
|
||||
---
|
||||
|
||||
## ✅ PASSO 10: Configurar Domínio (OPCIONAL)
|
||||
|
||||
Se quiser usar um domínio (ex: `smartagenda.com`) em vez do IP:
|
||||
|
||||
**10.1 - No servidor, copie e cole (Ubuntu/Debian):**
|
||||
|
||||
```bash
|
||||
sudo nano /etc/apache2/sites-available/smart-agenda.conf
|
||||
```
|
||||
|
||||
**10.2 - Cole este conteúdo (ajuste o domínio):**
|
||||
|
||||
```apache
|
||||
<VirtualHost *:80>
|
||||
ServerName seu-dominio.com
|
||||
ServerAlias www.seu-dominio.com
|
||||
DocumentRoot /var/www/html/smart-agenda
|
||||
<Directory /var/www/html/smart-agenda>
|
||||
Options -Indexes +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
ErrorLog ${APACHE_LOG_DIR}/smart-agenda-error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/smart-agenda-access.log combined
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
**10.3 - Salve:** `Ctrl+O`, `Enter`, `Ctrl+X`
|
||||
|
||||
**10.4 - Ative o site:**
|
||||
|
||||
```bash
|
||||
sudo a2ensite smart-agenda.conf
|
||||
sudo systemctl reload apache2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 SE ALGO DER ERRADO
|
||||
|
||||
### Problema: Página em branco
|
||||
|
||||
**No servidor, execute:**
|
||||
|
||||
```bash
|
||||
sudo chmod -R 755 /var/www/html/smart-agenda
|
||||
sudo chown -R www-data:www-data /var/www/html/smart-agenda
|
||||
```
|
||||
|
||||
### Problema: Rotas não funcionam (404)
|
||||
|
||||
**No servidor, execute:**
|
||||
|
||||
```bash
|
||||
sudo a2enmod rewrite
|
||||
sudo systemctl restart apache2
|
||||
ls -la /var/www/html/smart-agenda/.htaccess
|
||||
```
|
||||
|
||||
### Problema: Assets não carregam
|
||||
|
||||
**No servidor, verifique:**
|
||||
|
||||
```bash
|
||||
ls -la /var/www/html/smart-agenda/
|
||||
ls -la /var/www/html/smart-agenda/assets/
|
||||
```
|
||||
|
||||
**Deve ter arquivos CSS e JS na pasta assets/**
|
||||
|
||||
### Ver logs de erro:
|
||||
|
||||
**Ubuntu/Debian:**
|
||||
```bash
|
||||
sudo tail -f /var/log/apache2/error.log
|
||||
```
|
||||
|
||||
**CentOS/RHEL:**
|
||||
```bash
|
||||
sudo tail -f /var/log/httpd/error_log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ CHECKLIST FINAL
|
||||
|
||||
- [ ] Build criado no Mac (`npm run build`)
|
||||
- [ ] Arquivos copiados para o servidor
|
||||
- [ ] Arquivo `.htaccess` criado
|
||||
- [ ] Permissões configuradas
|
||||
- [ ] Módulo rewrite habilitado
|
||||
- [ ] Apache reiniciado
|
||||
- [ ] Aplicação acessível no navegador
|
||||
- [ ] Rotas funcionando
|
||||
|
||||
---
|
||||
|
||||
## 🎉 PRONTO!
|
||||
|
||||
Sua aplicação está publicada! Acesse `http://SEU-IP/smart-agenda/` no navegador.
|
||||
288
web/PASSO_A_PASSO_APACHE.md
Normal file
288
web/PASSO_A_PASSO_APACHE.md
Normal file
@@ -0,0 +1,288 @@
|
||||
# 📋 Passo a Passo - Publicar no Apache
|
||||
|
||||
Guia simples e direto para publicar a aplicação Smart Agenda no Apache.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ IMPORTANTE: Onde Executar os Comandos
|
||||
|
||||
- **Passos 1 e 2:** No seu **COMPUTADOR LOCAL** (onde está o código)
|
||||
- **Passos 3 em diante:** No **SERVIDOR** (via SSH)
|
||||
|
||||
---
|
||||
|
||||
## ✅ Passo 1: Criar o Build (NO SEU COMPUTADOR)
|
||||
|
||||
**⚠️ Execute isto no seu computador, não no servidor!**
|
||||
|
||||
Abra o terminal no seu computador e vá até a pasta do projeto:
|
||||
|
||||
```bash
|
||||
cd /Users/230417/SmartAgendaMobile/web
|
||||
npm run build
|
||||
```
|
||||
|
||||
**O que acontece:** Cria a pasta `dist/` com os arquivos otimizados para produção.
|
||||
|
||||
**Tempo estimado:** 1-2 minutos
|
||||
|
||||
**Verifique se a pasta foi criada:**
|
||||
```bash
|
||||
ls -la dist/
|
||||
```
|
||||
|
||||
Deve ver arquivos como `index.html` e uma pasta `assets/`.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Passo 2: Verificar o Build Localmente (Opcional - NO SEU COMPUTADOR)
|
||||
|
||||
**⚠️ Ainda no seu computador!**
|
||||
|
||||
Antes de enviar para o servidor, teste localmente:
|
||||
|
||||
```bash
|
||||
npm run preview
|
||||
```
|
||||
|
||||
Acesse `http://localhost:4173` no navegador para verificar se está tudo funcionando.
|
||||
|
||||
**Depois, pressione `Ctrl+C` para parar o servidor.**
|
||||
|
||||
---
|
||||
|
||||
## ✅ Passo 3: Conectar ao Servidor
|
||||
|
||||
Conecte-se ao seu servidor Apache via SSH:
|
||||
|
||||
```bash
|
||||
ssh usuario@seu-servidor.com
|
||||
```
|
||||
|
||||
**Substitua:**
|
||||
- `usuario` pelo seu usuário do servidor
|
||||
- `seu-servidor.com` pelo IP ou domínio do servidor
|
||||
|
||||
---
|
||||
|
||||
## ✅ Passo 4: Criar Diretório no Servidor
|
||||
|
||||
No servidor, crie a pasta onde a aplicação ficará:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /var/www/html/smart-agenda
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Passo 5: Copiar Arquivos para o Servidor
|
||||
|
||||
**⚠️ IMPORTANTE: Execute isto no SEU COMPUTADOR, não no servidor!**
|
||||
|
||||
**Opção A - Via SCP (Recomendado - Do seu computador):**
|
||||
|
||||
No seu computador, abra um terminal e execute:
|
||||
|
||||
```bash
|
||||
cd /Users/230417/SmartAgendaMobile/web
|
||||
scp -r dist/* santospap@seu-servidor.com:/var/www/html/smart-agenda/
|
||||
```
|
||||
|
||||
**Substitua `seu-servidor.com` pelo IP ou domínio do seu servidor.**
|
||||
|
||||
**Opção B - Via FTP/SFTP (FileZilla, WinSCP, etc.):**
|
||||
|
||||
1. No seu computador, abra o cliente FTP (FileZilla, WinSCP, etc.)
|
||||
2. Conecte-se ao servidor
|
||||
3. Navegue até `/var/www/html/` no servidor
|
||||
4. Crie a pasta `smart-agenda` se não existir
|
||||
5. No seu computador, navegue até `/Users/230417/SmartAgendaMobile/web/dist/`
|
||||
6. Faça upload de **TODOS** os arquivos e pastas da pasta `dist/` para `/var/www/html/smart-agenda/` no servidor
|
||||
|
||||
**Opção C - Se já tiver os arquivos no servidor:**
|
||||
|
||||
Se por algum motivo já tiver os arquivos em outro lugar no servidor:
|
||||
|
||||
```bash
|
||||
sudo cp -r /caminho/para/arquivos/* /var/www/html/smart-agenda/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Passo 6: Criar Arquivo .htaccess
|
||||
|
||||
No servidor, crie o arquivo `.htaccess`:
|
||||
|
||||
```bash
|
||||
sudo nano /var/www/html/smart-agenda/.htaccess
|
||||
```
|
||||
|
||||
**Cole este conteúdo:**
|
||||
|
||||
```apache
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
|
||||
# Não reescrever arquivos e diretórios existentes
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
|
||||
# Redirecionar tudo para index.html (SPA routing)
|
||||
RewriteRule . /index.html [L]
|
||||
</IfModule>
|
||||
```
|
||||
|
||||
**Para salvar:** Pressione `Ctrl+O`, depois `Enter`, depois `Ctrl+X`
|
||||
|
||||
---
|
||||
|
||||
## ✅ Passo 7: Configurar Permissões
|
||||
|
||||
No servidor, configure as permissões corretas:
|
||||
|
||||
```bash
|
||||
sudo chown -R www-data:www-data /var/www/html/smart-agenda
|
||||
sudo chmod -R 755 /var/www/html/smart-agenda
|
||||
```
|
||||
|
||||
**Nota:** Se usar CentOS/RHEL, use `apache:apache` em vez de `www-data:www-data`
|
||||
|
||||
---
|
||||
|
||||
## ✅ Passo 8: Habilitar Módulo Rewrite (Se necessário)
|
||||
|
||||
No servidor, habilite o módulo rewrite do Apache:
|
||||
|
||||
**Ubuntu/Debian:**
|
||||
```bash
|
||||
sudo a2enmod rewrite
|
||||
sudo systemctl restart apache2
|
||||
```
|
||||
|
||||
**CentOS/RHEL:**
|
||||
```bash
|
||||
# Geralmente já vem habilitado, mas verifique:
|
||||
sudo systemctl restart httpd
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Passo 9: Configurar Virtual Host (Opcional mas Recomendado)
|
||||
|
||||
Se quiser usar um domínio específico, configure um Virtual Host:
|
||||
|
||||
**Ubuntu/Debian:**
|
||||
```bash
|
||||
sudo nano /etc/apache2/sites-available/smart-agenda.conf
|
||||
```
|
||||
|
||||
**CentOS/RHEL:**
|
||||
```bash
|
||||
sudo nano /etc/httpd/conf.d/smart-agenda.conf
|
||||
```
|
||||
|
||||
**Cole este conteúdo (ajuste o domínio):**
|
||||
|
||||
```apache
|
||||
<VirtualHost *:80>
|
||||
ServerName seu-dominio.com
|
||||
ServerAlias www.seu-dominio.com
|
||||
|
||||
DocumentRoot /var/www/html/smart-agenda
|
||||
|
||||
<Directory /var/www/html/smart-agenda>
|
||||
Options -Indexes +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/smart-agenda-error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/smart-agenda-access.log combined
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
**Para Ubuntu/Debian, ative o site:**
|
||||
```bash
|
||||
sudo a2ensite smart-agenda.conf
|
||||
sudo systemctl reload apache2
|
||||
```
|
||||
|
||||
**Para CentOS/RHEL:**
|
||||
```bash
|
||||
sudo systemctl reload httpd
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Passo 10: Testar
|
||||
|
||||
1. Acesse no navegador:
|
||||
- `http://seu-servidor/smart-agenda/` (se não configurou domínio)
|
||||
- `http://seu-dominio.com` (se configurou domínio)
|
||||
|
||||
2. Teste as rotas:
|
||||
- `/login`
|
||||
- `/explorar`
|
||||
- `/registo`
|
||||
|
||||
3. Verifique o console do navegador (F12) para erros
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Troubleshooting Rápido
|
||||
|
||||
### Problema: Página em branco
|
||||
```bash
|
||||
# Verifique permissões
|
||||
sudo chmod -R 755 /var/www/html/smart-agenda
|
||||
sudo chown -R www-data:www-data /var/www/html/smart-agenda
|
||||
```
|
||||
|
||||
### Problema: Rotas não funcionam (404)
|
||||
```bash
|
||||
# Verifique se o módulo rewrite está habilitado
|
||||
sudo a2enmod rewrite
|
||||
sudo systemctl restart apache2
|
||||
|
||||
# Verifique se o .htaccess existe
|
||||
ls -la /var/www/html/smart-agenda/.htaccess
|
||||
```
|
||||
|
||||
### Problema: Assets não carregam
|
||||
```bash
|
||||
# Verifique se todos os arquivos foram copiados
|
||||
ls -la /var/www/html/smart-agenda/
|
||||
|
||||
# Deve ter pelo menos:
|
||||
# - index.html
|
||||
# - assets/ (pasta)
|
||||
```
|
||||
|
||||
### Ver logs de erro:
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo tail -f /var/log/apache2/error.log
|
||||
|
||||
# CentOS/RHEL
|
||||
sudo tail -f /var/log/httpd/error_log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Checklist Final
|
||||
|
||||
- [ ] Build criado (`npm run build`)
|
||||
- [ ] Arquivos copiados para `/var/www/html/smart-agenda/`
|
||||
- [ ] Arquivo `.htaccess` criado
|
||||
- [ ] Permissões configuradas
|
||||
- [ ] Módulo rewrite habilitado
|
||||
- [ ] Apache reiniciado
|
||||
- [ ] Aplicação acessível no navegador
|
||||
- [ ] Rotas funcionando corretamente
|
||||
|
||||
---
|
||||
|
||||
## 🎉 Pronto!
|
||||
|
||||
Sua aplicação está publicada! Se tiver problemas, verifique os logs do Apache.
|
||||
439
web/PUBLICAR_SERVIDOR.md
Normal file
439
web/PUBLICAR_SERVIDOR.md
Normal file
@@ -0,0 +1,439 @@
|
||||
# 🚀 Como Publicar a Aplicação Web no Servidor
|
||||
|
||||
Este guia explica como fazer o build e publicar a aplicação web Smart Agenda em um servidor de produção.
|
||||
|
||||
## 📦 Build de Produção
|
||||
|
||||
### 1. Criar o Build
|
||||
|
||||
Na pasta `web`, execute:
|
||||
|
||||
```bash
|
||||
cd web
|
||||
npm run build
|
||||
```
|
||||
|
||||
Isso criará uma pasta `dist/` com os arquivos otimizados para produção.
|
||||
|
||||
### 2. Verificar o Build Localmente
|
||||
|
||||
Antes de publicar, pode testar o build localmente:
|
||||
|
||||
```bash
|
||||
npm run preview
|
||||
```
|
||||
|
||||
Isso servirá o build em `http://localhost:4173` (porta padrão do Vite preview).
|
||||
|
||||
## 🌐 Opções de Publicação
|
||||
|
||||
### Opção 1: Servidor Estático (Nginx, Apache, etc.)
|
||||
|
||||
#### Nginx
|
||||
|
||||
1. Copie o conteúdo da pasta `dist/` para o diretório do servidor web:
|
||||
```bash
|
||||
cp -r web/dist/* /var/www/smart-agenda/
|
||||
```
|
||||
|
||||
2. Configure o Nginx para servir os arquivos estáticos:
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name seu-dominio.com;
|
||||
root /var/www/smart-agenda;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Reinicie o Nginx:
|
||||
```bash
|
||||
sudo systemctl restart nginx
|
||||
```
|
||||
|
||||
#### Apache (Instruções Detalhadas)
|
||||
|
||||
##### Passo 1: Criar o Build
|
||||
|
||||
Na pasta do projeto:
|
||||
```bash
|
||||
cd web
|
||||
npm run build
|
||||
```
|
||||
|
||||
Isso criará a pasta `dist/` com os arquivos otimizados.
|
||||
|
||||
##### Passo 2: Copiar Arquivos para o Servidor
|
||||
|
||||
**Opção A - Via SCP (se o servidor for remoto):**
|
||||
```bash
|
||||
scp -r web/dist/* usuario@seu-servidor.com:/var/www/html/smart-agenda/
|
||||
```
|
||||
|
||||
**Opção B - Via FTP/SFTP:**
|
||||
- Conecte-se ao servidor via FTP/SFTP
|
||||
- Navegue até `/var/www/html/` (ou o diretório configurado)
|
||||
- Crie a pasta `smart-agenda` se não existir
|
||||
- Faça upload de todos os arquivos da pasta `web/dist/`
|
||||
|
||||
**Opção C - No próprio servidor:**
|
||||
```bash
|
||||
# Se já estiver no servidor
|
||||
cp -r /caminho/para/projeto/web/dist/* /var/www/html/smart-agenda/
|
||||
```
|
||||
|
||||
##### Passo 3: Criar arquivo .htaccess
|
||||
|
||||
Crie um arquivo `.htaccess` na pasta `/var/www/html/smart-agenda/`:
|
||||
|
||||
```apache
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
|
||||
# Não reescrever arquivos e diretórios existentes
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
|
||||
# Redirecionar tudo para index.html (SPA routing)
|
||||
RewriteRule . /index.html [L]
|
||||
</IfModule>
|
||||
|
||||
# Habilitar compressão GZIP
|
||||
<IfModule mod_deflate.c>
|
||||
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/json
|
||||
</IfModule>
|
||||
|
||||
# Cache de arquivos estáticos
|
||||
<IfModule mod_expires.c>
|
||||
ExpiresActive On
|
||||
ExpiresByType image/jpg "access plus 1 year"
|
||||
ExpiresByType image/jpeg "access plus 1 year"
|
||||
ExpiresByType image/gif "access plus 1 year"
|
||||
ExpiresByType image/png "access plus 1 year"
|
||||
ExpiresByType text/css "access plus 1 month"
|
||||
ExpiresByType application/javascript "access plus 1 month"
|
||||
ExpiresByType application/pdf "access plus 1 month"
|
||||
ExpiresByType text/html "access plus 0 seconds"
|
||||
</IfModule>
|
||||
```
|
||||
|
||||
##### Passo 4: Configurar Virtual Host (Opcional mas Recomendado)
|
||||
|
||||
Crie ou edite o arquivo de configuração do Apache:
|
||||
|
||||
**Ubuntu/Debian:**
|
||||
```bash
|
||||
sudo nano /etc/apache2/sites-available/smart-agenda.conf
|
||||
```
|
||||
|
||||
**CentOS/RHEL:**
|
||||
```bash
|
||||
sudo nano /etc/httpd/conf.d/smart-agenda.conf
|
||||
```
|
||||
|
||||
Adicione a configuração:
|
||||
|
||||
```apache
|
||||
<VirtualHost *:80>
|
||||
ServerName seu-dominio.com
|
||||
ServerAlias www.seu-dominio.com
|
||||
|
||||
DocumentRoot /var/www/html/smart-agenda
|
||||
|
||||
<Directory /var/www/html/smart-agenda>
|
||||
Options -Indexes +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
|
||||
# Permitir arquivo .htaccess
|
||||
AllowOverride FileInfo
|
||||
</Directory>
|
||||
|
||||
# Logs
|
||||
ErrorLog ${APACHE_LOG_DIR}/smart-agenda-error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/smart-agenda-access.log combined
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
**Para Ubuntu/Debian, ative o site:**
|
||||
```bash
|
||||
sudo a2ensite smart-agenda.conf
|
||||
sudo systemctl reload apache2
|
||||
```
|
||||
|
||||
**Para CentOS/RHEL:**
|
||||
```bash
|
||||
sudo systemctl reload httpd
|
||||
```
|
||||
|
||||
##### Passo 5: Habilitar Módulos Necessários
|
||||
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo a2enmod rewrite
|
||||
sudo a2enmod headers
|
||||
sudo systemctl restart apache2
|
||||
|
||||
# CentOS/RHEL
|
||||
# Os módulos geralmente já vêm habilitados
|
||||
sudo systemctl restart httpd
|
||||
```
|
||||
|
||||
##### Passo 6: Verificar Permissões
|
||||
|
||||
```bash
|
||||
# Definir proprietário correto (ajuste conforme necessário)
|
||||
sudo chown -R www-data:www-data /var/www/html/smart-agenda
|
||||
|
||||
# Ou se usar outro usuário:
|
||||
sudo chown -R apache:apache /var/www/html/smart-agenda
|
||||
|
||||
# Definir permissões
|
||||
sudo chmod -R 755 /var/www/html/smart-agenda
|
||||
```
|
||||
|
||||
##### Passo 7: Testar
|
||||
|
||||
1. Acesse `http://seu-servidor/smart-agenda/` ou `http://seu-dominio.com`
|
||||
2. Verifique se a aplicação carrega
|
||||
3. Teste as rotas (ex: `/login`, `/explorar`)
|
||||
4. Verifique o console do navegador para erros
|
||||
|
||||
##### Configuração HTTPS (SSL/TLS) - Recomendado
|
||||
|
||||
Para produção, configure HTTPS usando Let's Encrypt:
|
||||
|
||||
```bash
|
||||
# Instalar Certbot
|
||||
sudo apt install certbot python3-certbot-apache # Ubuntu/Debian
|
||||
sudo yum install certbot python3-certbot-apache # CentOS/RHEL
|
||||
|
||||
# Obter certificado
|
||||
sudo certbot --apache -d seu-dominio.com -d www.seu-dominio.com
|
||||
```
|
||||
|
||||
##### Troubleshooting Apache
|
||||
|
||||
**Problema: Rotas não funcionam (404)**
|
||||
- Verifique se o módulo `rewrite` está habilitado
|
||||
- Confirme que `AllowOverride All` está no VirtualHost
|
||||
- Verifique se o `.htaccess` está no lugar correto
|
||||
|
||||
**Problema: Permissão negada**
|
||||
```bash
|
||||
sudo chmod -R 755 /var/www/html/smart-agenda
|
||||
sudo chown -R www-data:www-data /var/www/html/smart-agenda
|
||||
```
|
||||
|
||||
**Problema: Assets não carregam**
|
||||
- Verifique o caminho base no `vite.config.ts`
|
||||
- Confirme que todos os arquivos foram copiados
|
||||
- Verifique as permissões dos arquivos
|
||||
|
||||
**Ver logs de erro:**
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo tail -f /var/log/apache2/error.log
|
||||
|
||||
# CentOS/RHEL
|
||||
sudo tail -f /var/log/httpd/error_log
|
||||
```
|
||||
|
||||
### Opção 2: Vercel (Recomendado - Grátis)
|
||||
|
||||
1. Instale a CLI da Vercel:
|
||||
```bash
|
||||
npm i -g vercel
|
||||
```
|
||||
|
||||
2. Na pasta `web`, execute:
|
||||
```bash
|
||||
vercel
|
||||
```
|
||||
|
||||
3. Siga as instruções no terminal para configurar o projeto.
|
||||
|
||||
4. Para deploy automático, conecte o repositório GitHub na dashboard da Vercel.
|
||||
|
||||
### Opção 3: Netlify (Grátis)
|
||||
|
||||
1. Instale a CLI do Netlify:
|
||||
```bash
|
||||
npm i -g netlify-cli
|
||||
```
|
||||
|
||||
2. Na pasta `web`, execute:
|
||||
```bash
|
||||
netlify deploy --prod --dir=dist
|
||||
```
|
||||
|
||||
3. Ou arraste a pasta `dist/` para o site do Netlify.
|
||||
|
||||
### Opção 4: GitHub Pages
|
||||
|
||||
1. Instale o plugin do Vite:
|
||||
```bash
|
||||
npm install --save-dev vite-plugin-gh-pages
|
||||
```
|
||||
|
||||
2. Atualize `vite.config.ts`:
|
||||
```typescript
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import ghPages from 'vite-plugin-gh-pages';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), ghPages()],
|
||||
base: '/SmartAgendaMobile/', // Nome do repositório
|
||||
server: {
|
||||
port: 5173,
|
||||
host: '0.0.0.0',
|
||||
strictPort: false,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
3. Adicione script no `package.json`:
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"deploy": "npm run build && gh-pages -d dist"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. Execute:
|
||||
```bash
|
||||
npm run deploy
|
||||
```
|
||||
|
||||
### Opção 5: Servidor Node.js com Express
|
||||
|
||||
1. Instale as dependências:
|
||||
```bash
|
||||
npm install express
|
||||
```
|
||||
|
||||
2. Crie um arquivo `server.js` na pasta `web`:
|
||||
```javascript
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const app = express();
|
||||
|
||||
app.use(express.static(path.join(__dirname, 'dist')));
|
||||
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
|
||||
});
|
||||
|
||||
const port = process.env.PORT || 3000;
|
||||
app.listen(port, () => {
|
||||
console.log(`Servidor rodando na porta ${port}`);
|
||||
});
|
||||
```
|
||||
|
||||
3. Adicione ao `package.json`:
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"start:prod": "node server.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. Execute:
|
||||
```bash
|
||||
npm run build
|
||||
npm run start:prod
|
||||
```
|
||||
|
||||
### Opção 6: Docker
|
||||
|
||||
1. Crie um `Dockerfile` na pasta `web`:
|
||||
```dockerfile
|
||||
FROM node:18-alpine as build
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:alpine
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
```
|
||||
|
||||
2. Crie `nginx.conf`:
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Build e execute:
|
||||
```bash
|
||||
docker build -t smart-agenda-web .
|
||||
docker run -p 80:80 smart-agenda-web
|
||||
```
|
||||
|
||||
## ⚙️ Configurações Importantes
|
||||
|
||||
### Variáveis de Ambiente
|
||||
|
||||
Se precisar de variáveis de ambiente, crie um arquivo `.env.production`:
|
||||
|
||||
```env
|
||||
VITE_API_URL=https://api.seudominio.com
|
||||
VITE_APP_NAME=Smart Agenda
|
||||
```
|
||||
|
||||
### Base Path
|
||||
|
||||
Se a aplicação não estiver na raiz do domínio, configure o `base` no `vite.config.ts`:
|
||||
|
||||
```typescript
|
||||
export default defineConfig({
|
||||
base: '/smart-agenda/',
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
## 🔍 Verificações Pós-Deploy
|
||||
|
||||
1. ✅ Verifique se todos os assets carregam corretamente
|
||||
2. ✅ Teste todas as rotas (SPA routing)
|
||||
3. ✅ Verifique o console do navegador para erros
|
||||
4. ✅ Teste em diferentes navegadores
|
||||
5. ✅ Verifique responsividade mobile
|
||||
|
||||
## 📝 Notas
|
||||
|
||||
- O build cria arquivos estáticos (HTML, CSS, JS)
|
||||
- Não é necessário Node.js em produção (exceto se usar servidor Express)
|
||||
- Certifique-se de que o servidor está configurado para servir `index.html` em todas as rotas (SPA routing)
|
||||
- Para HTTPS, configure SSL/TLS no servidor
|
||||
|
||||
## 🆘 Problemas Comuns
|
||||
|
||||
### Rotas não funcionam após refresh
|
||||
- Configure o servidor para redirecionar todas as rotas para `index.html`
|
||||
|
||||
### Assets não carregam
|
||||
- Verifique o `base` path no `vite.config.ts`
|
||||
- Certifique-se de que os arquivos estão no diretório correto
|
||||
|
||||
### Erro 404
|
||||
- Configure o servidor para servir `index.html` em todas as rotas
|
||||
@@ -34,7 +34,12 @@ import {
|
||||
ChevronDown,
|
||||
UserPlus,
|
||||
Globe,
|
||||
TrendingUp,
|
||||
HelpCircle,
|
||||
MessageCircle,
|
||||
Settings,
|
||||
Store,
|
||||
Clock,
|
||||
ArrowRight,
|
||||
} from 'lucide-react';
|
||||
|
||||
const periods: Record<string, (date: Date) => boolean> = {
|
||||
@@ -107,34 +112,39 @@ export default function Dashboard() {
|
||||
// Agendamentos concluídos (histórico)
|
||||
const completedAppointments = allShopAppointments.filter((a) => a.status === 'concluido');
|
||||
|
||||
// Estatísticas para lista de marcações
|
||||
const todayAppointments = appointments.filter((a) => {
|
||||
// Estatísticas para lista de marcações (do dia selecionado)
|
||||
const selectedDateAppointments = appointments.filter((a) => {
|
||||
if (a.shopId !== shop.id) return false;
|
||||
const aptDate = new Date(a.date.replace(' ', 'T'));
|
||||
const today = new Date();
|
||||
return (
|
||||
aptDate.getDate() === today.getDate() &&
|
||||
aptDate.getMonth() === today.getMonth() &&
|
||||
aptDate.getFullYear() === today.getFullYear()
|
||||
aptDate.getDate() === selectedDate.getDate() &&
|
||||
aptDate.getMonth() === selectedDate.getMonth() &&
|
||||
aptDate.getFullYear() === selectedDate.getFullYear()
|
||||
);
|
||||
});
|
||||
|
||||
const totalBookingsToday = todayAppointments.filter((a) => includeCancelled || a.status !== 'cancelado').length;
|
||||
const totalBookingsToday = selectedDateAppointments.filter((a) => includeCancelled || a.status !== 'cancelado').length;
|
||||
|
||||
const newClientsToday = useMemo(() => {
|
||||
const clientIds = new Set(todayAppointments.map((a) => a.customerId));
|
||||
const clientIds = new Set(selectedDateAppointments.map((a) => a.customerId));
|
||||
return clientIds.size;
|
||||
}, [todayAppointments]);
|
||||
const onlineBookingsToday = todayAppointments.filter((a) => a.status !== 'cancelado').length;
|
||||
}, [selectedDateAppointments]);
|
||||
|
||||
const onlineBookingsToday = selectedDateAppointments.filter((a) => a.status !== 'cancelado').length;
|
||||
|
||||
const occupancyRate = useMemo(() => {
|
||||
// Calcular ocupação baseada em slots disponíveis (8h-18h = 20 slots de 30min)
|
||||
const totalSlots = 20;
|
||||
const bookedSlots = todayAppointments.filter((a) => a.status !== 'cancelado').length;
|
||||
const bookedSlots = selectedDateAppointments.filter((a) => a.status !== 'cancelado').length;
|
||||
return Math.round((bookedSlots / totalSlots) * 100);
|
||||
}, [todayAppointments]);
|
||||
}, [selectedDateAppointments]);
|
||||
|
||||
// Comparação com semana passada (simplificado - sempre 0% por enquanto)
|
||||
const comparisonPercent = 0;
|
||||
|
||||
// Filtrar agendamentos para lista
|
||||
const filteredAppointments = useMemo(() => {
|
||||
let filtered = shopAppointments;
|
||||
let filtered = selectedDateAppointments;
|
||||
|
||||
if (!includeCancelled) {
|
||||
filtered = filtered.filter((a) => a.status !== 'cancelado');
|
||||
@@ -157,7 +167,8 @@ export default function Dashboard() {
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}, [shopAppointments, includeCancelled, searchQuery, shop.services, shop.barbers, users]);
|
||||
}, [selectedDateAppointments, includeCancelled, searchQuery, shop.services, shop.barbers, users]);
|
||||
|
||||
// Pedidos apenas com produtos (não serviços)
|
||||
const shopOrders = orders.filter(
|
||||
(o) => o.shopId === shop.id && periodMatch(new Date(o.createdAt)) && o.items.some((item) => item.type === 'product')
|
||||
@@ -295,109 +306,238 @@ export default function Dashboard() {
|
||||
{/* Tab Content */}
|
||||
{activeTab === 'overview' && (
|
||||
<div className="space-y-6">
|
||||
{/* Stats Cards */}
|
||||
<div className="grid md:grid-cols-4 gap-4">
|
||||
<Card className="p-5 bg-gradient-to-br from-amber-50 to-white">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="p-2 bg-amber-500 rounded-lg text-white">
|
||||
<TrendingUp size={20} />
|
||||
{/* Saudação */}
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-2xl font-bold text-slate-900">
|
||||
{new Date().toLocaleDateString('pt-PT', { weekday: 'long', day: 'numeric', month: 'long' })}
|
||||
</h1>
|
||||
<p className="text-slate-600">
|
||||
{(() => {
|
||||
const hour = new Date().getHours();
|
||||
if (hour < 12) return 'Bom dia';
|
||||
if (hour < 18) return 'Boa tarde';
|
||||
return 'Boa noite';
|
||||
})()}, {user?.name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards Principais */}
|
||||
<div className="grid md:grid-cols-3 gap-4">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="p-3 bg-indigo-500 rounded-lg text-white">
|
||||
<Calendar size={24} />
|
||||
</div>
|
||||
<Badge color="amber" variant="soft">Período</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 mb-1">Faturamento</p>
|
||||
<p className="text-2xl font-bold text-amber-700">{currency(totalRevenue)}</p>
|
||||
<p className="text-sm text-slate-600 mb-1">Total de reservas</p>
|
||||
<p className="text-3xl font-bold text-slate-900">{allShopAppointments.length}</p>
|
||||
<p className="text-xs text-slate-500 mt-2">Reservas da plataforma: {allShopAppointments.length}</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-5">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="p-2 bg-blue-500 rounded-lg text-white">
|
||||
<Calendar size={20} />
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="p-3 bg-blue-500 rounded-lg text-white">
|
||||
<Globe size={24} />
|
||||
</div>
|
||||
<Badge color="amber" variant="soft">{pendingAppts}</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 mb-1">Pendentes</p>
|
||||
<p className="text-2xl font-bold text-slate-900">{pendingAppts}</p>
|
||||
<p className="text-sm text-slate-600 mb-1">Reservas online</p>
|
||||
<p className="text-3xl font-bold text-slate-900">{allShopAppointments.length}</p>
|
||||
<p className="text-xs text-slate-500 mt-2">Marcações feitas pela plataforma</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-5">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="p-2 bg-emerald-500 rounded-lg text-white">
|
||||
<Calendar size={20} />
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="p-3 bg-green-500 rounded-lg text-white">
|
||||
<UserPlus size={24} />
|
||||
</div>
|
||||
<Badge color="green" variant="soft">{confirmedAppts}</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 mb-1">Confirmados</p>
|
||||
<p className="text-2xl font-bold text-slate-900">{confirmedAppts}</p>
|
||||
</Card>
|
||||
|
||||
<Card className={`p-5 ${lowStock.length > 0 ? 'bg-amber-50 border-amber-200' : ''}`}>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className={`p-2 rounded-lg text-white ${lowStock.length > 0 ? 'bg-amber-500' : 'bg-slate-500'}`}>
|
||||
<AlertTriangle size={20} />
|
||||
</div>
|
||||
{lowStock.length > 0 && <Badge color="amber" variant="solid">{lowStock.length}</Badge>}
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 mb-1">Stock baixo</p>
|
||||
<p className={`text-2xl font-bold ${lowStock.length > 0 ? 'text-amber-700' : 'text-slate-900'}`}>{lowStock.length}</p>
|
||||
<p className="text-sm text-slate-600 mb-1">Novos clientes</p>
|
||||
<p className="text-3xl font-bold text-slate-900">
|
||||
{new Set(allShopAppointments.map(a => a.customerId)).size}
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mt-2">Clientes únicos</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Charts */}
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-bold text-slate-900 mb-4">Serviços vs Produtos</h3>
|
||||
<div className="h-64">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={comparisonData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="name" />
|
||||
<Tooltip />
|
||||
<Bar dataKey="value" fill="#f59e0b" radius={[6, 6, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Card className="p-5">
|
||||
<h3 className="text-base font-bold text-slate-900 mb-3">Top 5 Serviços</h3>
|
||||
<div className="space-y-2">
|
||||
{topServices.length > 0 ? (
|
||||
topServices.map((s, idx) => (
|
||||
<div key={s.name} className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-slate-400 font-bold">#{idx + 1}</span>
|
||||
<span className="text-slate-700">{s.name}</span>
|
||||
</div>
|
||||
<Badge color="amber">{s.qty} vendas</Badge>
|
||||
{/* Layout em duas colunas */}
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
{/* Coluna Principal - Esquerda */}
|
||||
<div className="md:col-span-2 space-y-6">
|
||||
{/* Reservas de Hoje */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 bg-indigo-100 rounded-lg">
|
||||
<Search size={20} className="text-indigo-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-slate-900">Reservas de hoje</h3>
|
||||
<p className="text-sm text-slate-600">Verá aqui as reservas de hoje assim que chegarem</p>
|
||||
</div>
|
||||
</div>
|
||||
{(() => {
|
||||
const today = new Date();
|
||||
const todayAppts = allShopAppointments.filter(a => {
|
||||
const aptDate = new Date(a.date.replace(' ', 'T'));
|
||||
return aptDate.toDateString() === today.toDateString();
|
||||
});
|
||||
|
||||
if (todayAppts.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-8">
|
||||
<Calendar size={48} className="mx-auto text-slate-300 mb-3" />
|
||||
<p className="text-slate-600 font-medium mb-2">Sem reservas hoje</p>
|
||||
<Button variant="outline" size="sm" onClick={() => setActiveTab('appointments')}>
|
||||
Ir para o calendário
|
||||
<ArrowRight size={16} className="ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-sm text-slate-500">Sem vendas no período</p>
|
||||
)}
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{todayAppts.slice(0, 3).map(a => {
|
||||
const svc = shop.services.find(s => s.id === a.serviceId);
|
||||
const barber = shop.barbers.find(b => b.id === a.barberId);
|
||||
const customer = users.find(u => u.id === a.customerId);
|
||||
const aptDate = new Date(a.date.replace(' ', 'T'));
|
||||
const timeStr = aptDate.toLocaleTimeString('pt-PT', { hour: '2-digit', minute: '2-digit' });
|
||||
|
||||
return (
|
||||
<div key={a.id} className="flex items-center justify-between p-3 border border-slate-200 rounded-lg hover:bg-slate-50">
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-slate-900">{customer?.name || 'Cliente'}</p>
|
||||
<p className="text-sm text-slate-600">{timeStr} · {svc?.name || 'Serviço'}</p>
|
||||
</div>
|
||||
<Badge color={a.status === 'pendente' ? 'amber' : a.status === 'confirmado' ? 'green' : 'red'}>
|
||||
{a.status}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{todayAppts.length > 3 && (
|
||||
<Button variant="outline" size="sm" className="w-full" onClick={() => setActiveTab('appointments')}>
|
||||
Ver todas ({todayAppts.length})
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Coluna Lateral - Direita */}
|
||||
<div className="space-y-6">
|
||||
{/* Ajuda */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-bold text-slate-900 mb-4">Tem alguma pergunta?</h3>
|
||||
<div className="space-y-3">
|
||||
<button className="w-full flex items-center gap-3 p-3 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors text-left">
|
||||
<div className="p-2 bg-indigo-100 rounded-lg">
|
||||
<HelpCircle size={18} className="text-indigo-600" />
|
||||
</div>
|
||||
<span className="font-medium text-slate-900">Centro de ajuda</span>
|
||||
</button>
|
||||
<button className="w-full flex items-center gap-3 p-3 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors text-left">
|
||||
<div className="p-2 bg-blue-100 rounded-lg">
|
||||
<MessageCircle size={18} className="text-blue-600" />
|
||||
</div>
|
||||
<span className="font-medium text-slate-900">Contacte-nos</span>
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-5">
|
||||
<h3 className="text-base font-bold text-slate-900 mb-3">Top 5 Produtos</h3>
|
||||
<div className="space-y-2">
|
||||
{topProducts.length > 0 ? (
|
||||
topProducts.map((p, idx) => (
|
||||
<div key={p.name} className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-slate-400 font-bold">#{idx + 1}</span>
|
||||
<span className="text-slate-700">{p.name}</span>
|
||||
</div>
|
||||
<Badge color="amber">{p.qty} vendas</Badge>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-sm text-slate-500">Sem vendas no período</p>
|
||||
)}
|
||||
{/* Atalhos */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-bold text-slate-900 mb-4">Atalhos</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
onClick={() => setActiveTab('services')}
|
||||
className="flex flex-col items-center gap-2 p-4 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
<div className="p-2 bg-indigo-100 rounded-lg">
|
||||
<Scissors size={20} className="text-indigo-600" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-slate-900">Serviços</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('appointments')}
|
||||
className="flex flex-col items-center gap-2 p-4 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
<div className="p-2 bg-blue-100 rounded-lg">
|
||||
<Users size={20} className="text-blue-600" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-slate-900">Clientes</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('appointments')}
|
||||
className="flex flex-col items-center gap-2 p-4 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
<div className="p-2 bg-green-100 rounded-lg">
|
||||
<Calendar size={20} className="text-green-600" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-slate-900">Calendário</span>
|
||||
</button>
|
||||
<button className="flex flex-col items-center gap-2 p-4 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors">
|
||||
<div className="p-2 bg-purple-100 rounded-lg">
|
||||
<Settings size={20} className="text-purple-600" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-slate-900">Definições</span>
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Próximos Agendamentos */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-bold text-slate-900 mb-4">Seguinte</h3>
|
||||
{(() => {
|
||||
const upcoming = allShopAppointments
|
||||
.filter(a => {
|
||||
const aptDate = new Date(a.date.replace(' ', 'T'));
|
||||
return aptDate > new Date() && a.status !== 'cancelado';
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const dateA = new Date(a.date.replace(' ', 'T'));
|
||||
const dateB = new Date(b.date.replace(' ', 'T'));
|
||||
return dateA.getTime() - dateB.getTime();
|
||||
})
|
||||
.slice(0, 3);
|
||||
|
||||
if (upcoming.length === 0) {
|
||||
return (
|
||||
<p className="text-sm text-slate-500 text-center py-4">Sem agendamentos futuros</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{upcoming.map(a => {
|
||||
const svc = shop.services.find(s => s.id === a.serviceId);
|
||||
const barber = shop.barbers.find(b => b.id === a.barberId);
|
||||
const customer = users.find(u => u.id === a.customerId);
|
||||
const aptDate = new Date(a.date.replace(' ', 'T'));
|
||||
const timeStr = aptDate.toLocaleTimeString('pt-PT', { hour: '2-digit', minute: '2-digit' });
|
||||
|
||||
return (
|
||||
<div key={a.id} className="flex items-start gap-3 p-3 border border-slate-200 rounded-lg">
|
||||
<div className="p-1.5 bg-indigo-100 rounded">
|
||||
<Clock size={14} className="text-indigo-600" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-semibold text-slate-900 truncate">{customer?.name || 'Cliente'}</p>
|
||||
<p className="text-sm text-slate-600">{timeStr}</p>
|
||||
<p className="text-xs text-slate-500 truncate">{svc?.name || 'Serviço'}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -442,10 +582,15 @@ export default function Dashboard() {
|
||||
<div className="p-2 bg-indigo-500 rounded-lg text-white">
|
||||
<Calendar size={20} />
|
||||
</div>
|
||||
<span className="text-xs text-slate-500">0%</span>
|
||||
<span className={`text-xs ${comparisonPercent >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{comparisonPercent >= 0 ? '+' : ''}{comparisonPercent}%
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 mb-1">Total de marcações</p>
|
||||
<p className="text-2xl font-bold text-slate-900">{totalBookingsToday}</p>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Comparado com {totalBookingsToday} no mesmo dia da semana passada
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-5">
|
||||
@@ -453,10 +598,15 @@ export default function Dashboard() {
|
||||
<div className="p-2 bg-green-500 rounded-lg text-white">
|
||||
<UserPlus size={20} />
|
||||
</div>
|
||||
<span className="text-xs text-slate-500">0%</span>
|
||||
<span className={`text-xs ${comparisonPercent >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{comparisonPercent >= 0 ? '+' : ''}{comparisonPercent}%
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 mb-1">Novos clientes</p>
|
||||
<p className="text-2xl font-bold text-slate-900">{newClientsToday}</p>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Comparado com {newClientsToday} no mesmo dia da semana passada
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-5">
|
||||
@@ -464,10 +614,15 @@ export default function Dashboard() {
|
||||
<div className="p-2 bg-blue-500 rounded-lg text-white">
|
||||
<Globe size={20} />
|
||||
</div>
|
||||
<span className="text-xs text-slate-500">0%</span>
|
||||
<span className={`text-xs ${comparisonPercent >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{comparisonPercent >= 0 ? '+' : ''}{comparisonPercent}%
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 mb-1">Marcações online</p>
|
||||
<p className="text-2xl font-bold text-slate-900">{onlineBookingsToday}</p>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Comparado com {onlineBookingsToday} no mesmo dia da semana passada
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-5">
|
||||
@@ -475,10 +630,15 @@ export default function Dashboard() {
|
||||
<div className="p-2 bg-purple-500 rounded-lg text-white">
|
||||
<TrendingUp size={20} />
|
||||
</div>
|
||||
<span className="text-xs text-slate-500">0%</span>
|
||||
<span className={`text-xs ${comparisonPercent >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{comparisonPercent >= 0 ? '+' : ''}{comparisonPercent}%
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 mb-1">Ocupação</p>
|
||||
<p className="text-2xl font-bold text-slate-900">{occupancyRate}%</p>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Comparado com {occupancyRate}% no mesmo dia da semana passada
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -489,7 +649,7 @@ export default function Dashboard() {
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={18} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Pesquisar por cliente, serviço ou barbeiro..."
|
||||
placeholder="Pesquisar por cliente"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full rounded-lg border border-slate-300 bg-white px-10 py-2.5 text-sm text-slate-900 placeholder:text-slate-400 focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/30 transition-all"
|
||||
@@ -528,7 +688,7 @@ export default function Dashboard() {
|
||||
}}>
|
||||
<ChevronLeft size={18} />
|
||||
</Button>
|
||||
<div className="text-sm font-medium text-slate-700">
|
||||
<div className="text-sm font-medium text-slate-700 px-3">
|
||||
{selectedDate.toLocaleDateString('pt-PT', {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
@@ -630,7 +790,7 @@ export default function Dashboard() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-16">
|
||||
<Calendar size={64} className="mx-auto text-slate-300 mb-4" />
|
||||
<Calendar size={64} className="mx-auto text-indigo-300 mb-4" />
|
||||
<p className="text-lg font-semibold text-slate-900 mb-2">Sem reservas</p>
|
||||
<p className="text-sm text-slate-600 max-w-md mx-auto">
|
||||
Ambas as suas reservas online e manuais aparecerão aqui
|
||||
|
||||
@@ -90,25 +90,25 @@ export default function Landing() {
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{[
|
||||
{
|
||||
icon: Calendar,
|
||||
{[
|
||||
{
|
||||
icon: Calendar,
|
||||
title: 'Agendamentos Inteligentes',
|
||||
desc: 'Escolha serviço, barbeiro, data e horário com validação de slots em tempo real. Notificações automáticas.',
|
||||
color: 'from-blue-500 to-blue-600'
|
||||
},
|
||||
{
|
||||
icon: ShoppingBag,
|
||||
title: 'Carrinho Inteligente',
|
||||
color: 'from-blue-500 to-blue-600'
|
||||
},
|
||||
{
|
||||
icon: ShoppingBag,
|
||||
title: 'Carrinho Inteligente',
|
||||
desc: 'Produtos e serviços agrupados por barbearia, checkout rápido e seguro. Histórico completo de compras.',
|
||||
color: 'from-emerald-500 to-emerald-600'
|
||||
},
|
||||
{
|
||||
icon: BarChart3,
|
||||
title: 'Painel Completo',
|
||||
color: 'from-emerald-500 to-emerald-600'
|
||||
},
|
||||
{
|
||||
icon: BarChart3,
|
||||
title: 'Painel Completo',
|
||||
desc: 'Faturamento, agendamentos, pedidos e análises detalhadas. Tudo no controle da sua barbearia.',
|
||||
color: 'from-purple-500 to-purple-600'
|
||||
},
|
||||
color: 'from-purple-500 to-purple-600'
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: 'Gestão de Barbeiros',
|
||||
@@ -127,17 +127,17 @@ export default function Landing() {
|
||||
desc: 'Dados protegidos, pagamentos seguros e backup automático. Conformidade com LGPD.',
|
||||
color: 'from-rose-500 to-rose-600'
|
||||
},
|
||||
].map((feature) => (
|
||||
<Card key={feature.title} hover className="p-6 space-y-4 group">
|
||||
<div className={`inline-flex p-3 rounded-xl bg-gradient-to-br ${feature.color} text-white shadow-lg group-hover:scale-110 transition-transform duration-200`}>
|
||||
<feature.icon size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-slate-900 mb-2">{feature.title}</h3>
|
||||
<p className="text-sm text-slate-600 leading-relaxed">{feature.desc}</p>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
].map((feature) => (
|
||||
<Card key={feature.title} hover className="p-6 space-y-4 group">
|
||||
<div className={`inline-flex p-3 rounded-xl bg-gradient-to-br ${feature.color} text-white shadow-lg group-hover:scale-110 transition-transform duration-200`}>
|
||||
<feature.icon size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-slate-900 mb-2">{feature.title}</h3>
|
||||
<p className="text-sm text-slate-600 leading-relaxed">{feature.desc}</p>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user