Compare commits

..

22 Commits

Author SHA1 Message Date
5673eb5255 🚀 Auto-deploy: Acesso direto implementado (Bypass Auth) e correção de chamadas .auth 2026-04-03 21:20:54 +00:00
556a2ace41 docs: implement Antigravity global rules 2026-04-03 21:11:01 +00:00
d5a2b570ec primeiras_alteracoes 2026-04-03 19:58:41 +00:00
Marcos
bc70aaaa5c chore: expand vscode settings to suppress all css at-rule warnings 2026-03-23 08:02:18 -03:00
Marcos
ab71f9abc8 fix: add .dockerignore and optimize build flags for VPS memory constraints 2026-03-23 08:01:49 -03:00
Marcos
2261da27ad fix: regenerate lockfile for consistency and update docker build flags 2026-03-23 08:01:05 -03:00
Marcos
aaeab42050 fix: downgrade eslint to v9 for compatibility and fix docker build in coolify 2026-03-23 07:55:17 -03:00
Marcos
25c75df1fd chore: add vscode settings to ignore tailwind v4 at-rules warnings 2026-03-23 07:48:42 -03:00
Marcos
45c0de3605 fix: resolve cascading renders in hooks and remove any types 2026-03-23 07:48:14 -03:00
Marcos
0ad29893d0 refactor: update packages to React 19, Vite 8 and Tailwind 4 and modernize configs 2026-03-23 07:40:32 -03:00
Marcos
c9c7f40a0b Config: Switched app to use 'rdo' schema in Supabase. 2026-03-22 22:19:58 -03:00
Marcos
e09a4f8389 Build: Updated Dockerfile to Node 22 + Nginx (Standard for VPS). 2026-03-22 21:56:41 -03:00
Marcos
e198eb3b09 Refactor: Cleaned up Vercel, Netlify, and other platform traces. Prepared for VPS deploy. 2026-03-22 21:55:34 -03:00
c9e75fbe2c docs: Guia para configurar Supabase e Google Cloud para Vercel 2026-02-24 08:54:22 -03:00
8bda3377b9 fix: Adicionar vercel.json com rewrites para SPA routing 2026-02-24 08:53:39 -03:00
0897943b5d docs: Guia para corrigir variáveis de ambiente na Vercel 2026-02-24 08:46:51 -03:00
b6e4f14bf9 fix: Remover vercel.json completamente - usar configuração padrão da Vercel 2026-02-24 08:29:49 -03:00
958ca0fb92 fix: Remover headers do vercel.json que causavam erro de validação 2026-02-24 08:28:22 -03:00
98ab91c21a newfirtsCommit 2026-02-24 08:17:51 -03:00
569747f166 fix: Simplificar vercel.json para resolver problemas de build 2026-02-24 08:14:53 -03:00
da6b1c8847 feat: Preparar app para deploy na Vercel (mantendo Netlify) 2026-02-24 08:08:11 -03:00
d028e51615 docs: Guia de teste de variáveis no navegador 2026-02-24 07:50:32 -03:00
71 changed files with 2837 additions and 7553 deletions

22
.agent/rules/GEMINI.md Normal file
View File

@@ -0,0 +1,22 @@
---
trigger: always_on
---
# GEMINI.md - Antigravity Kit (Auto-Generated)
## TIER 0: UNIVERSAL RULES (Always Active)
### 🌍 Global User Rules (MANDATORY)
1. **Idioma**: Conversar e desenvolver sempre em **Português do Brasil**.
2. **Automação Terminais**: Executar comandos de terminal, leitura de arquivos e edições de forma automática. Comandos de desenvolvimento (build, install, lint, git, etc.) são sempre 'SafeToAutoRun'.
3. **Memória & Monitoramento**: Após cada evolução ou atualização no app, é OBRIGATÓRIO:
- Atualizar a memória do app em '/root/Antigravity_Memory/knowledge/{AppName}.md'.
- Atualizar o status e credenciais no 'app.py' do Cronos Dashboard.
- Realizar 'git commit', 'git push' e executar o 'update.sh' do Cronos para deploy automático.
### 🌐 Language Handling
---

13
.dockerignore Normal file
View File

@@ -0,0 +1,13 @@
node_modules
dist
.git
.vscode
*.log
.env
.env.local
.vercel
ios
android
out
build
tmp

View File

@@ -1,110 +0,0 @@
# Configuração de Secrets para GitHub Actions
Para que a automação funcione corretamente, você precisa configurar os seguintes secrets no seu repositório GitHub:
## Como Configurar Secrets
1. Vá para o seu repositório no GitHub
2. Clique em **Settings** (Configurações)
3. No menu lateral, clique em **Secrets and variables** > **Actions**
4. Clique em **New repository secret**
5. Adicione cada secret listado abaixo
## Secrets Necessários
### 🔑 REMOTE_REPO_TOKEN
**Descrição:** Token de acesso pessoal para o repositório remoto `https://github.com/Reifonas/TS_RDO.git`
**Como obter:**
1. Vá para GitHub Settings > Developer settings > Personal access tokens > Tokens (classic)
2. Clique em "Generate new token (classic)"
3. Selecione as permissões:
- `repo` (acesso completo a repositórios)
- `workflow` (atualizar workflows)
- `write:packages` (se usar packages)
4. Copie o token gerado
5. Cole no secret `REMOTE_REPO_TOKEN`
### 🌐 NETLIFY_AUTH_TOKEN (Opcional)
**Descrição:** Token de autenticação do Netlify para deploy automático
**Como obter:**
1. Faça login no Netlify
2. Vá para User settings > Applications > Personal access tokens
3. Clique em "New access token"
4. Dê um nome e clique em "Generate token"
5. Copie o token e cole no secret `NETLIFY_AUTH_TOKEN`
### 🆔 NETLIFY_SITE_ID (Opcional)
**Descrição:** ID do site no Netlify
**Como obter:**
1. No dashboard do Netlify, clique no seu site
2. Vá para Site settings > General > Site details
3. Copie o "Site ID"
4. Cole no secret `NETLIFY_SITE_ID`
## Verificação da Configuração
Após configurar os secrets, você pode testar a automação:
1. **Teste Manual:**
- Vá para Actions no seu repositório
- Clique em "Auto Sync and Deploy"
- Clique em "Run workflow"
- Marque "Force deploy" se quiser forçar
2. **Teste Automático:**
- Faça qualquer alteração no código
- Commit e push para a branch main
- A action será executada automaticamente
## Estrutura dos Secrets
```
Repository Secrets:
├── REMOTE_REPO_TOKEN # Token para repositório remoto (OBRIGATÓRIO)
├── NETLIFY_AUTH_TOKEN # Token Netlify (opcional)
├── NETLIFY_SITE_ID # ID do site Netlify (opcional)
└── GITHUB_TOKEN # Automático (não precisa configurar)
```
## Troubleshooting
### ❌ Erro: "Authentication failed"
- Verifique se o `REMOTE_REPO_TOKEN` está correto
- Confirme se o token tem as permissões necessárias
- Verifique se o token não expirou
### ❌ Erro: "Repository not found"
- Confirme se o repositório `Reifonas/TS_RDO` existe
- Verifique se o token tem acesso ao repositório
### ❌ Erro de Deploy Netlify
- Verifique se `NETLIFY_AUTH_TOKEN` e `NETLIFY_SITE_ID` estão corretos
- Confirme se o site existe no Netlify
## Logs e Monitoramento
Para acompanhar a execução:
1. Vá para **Actions** no seu repositório
2. Clique na execução desejada
3. Expanda os jobs para ver os logs detalhados
## Segurança
⚠️ **IMPORTANTE:**
- Nunca compartilhe seus tokens
- Use tokens com permissões mínimas necessárias
- Revogue tokens antigos quando não precisar mais
- Monitore o uso dos tokens regularmente
## Frequência de Execução
A action executa:
- ✅ A cada push na branch main ou develop
- ✅ A cada pull request para main
- ✅ A cada 30 minutos (agendado)
- ✅ Manualmente quando solicitado
Para alterar a frequência, edite o arquivo `.github/workflows/auto-sync-deploy.yml`

View File

@@ -1,263 +0,0 @@
name: Auto Sync and Deploy
# Triggers para execução da action
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
schedule:
# Executa a cada 30 minutos
- cron: '*/30 * * * *'
workflow_dispatch:
inputs:
force_deploy:
description: 'Force deploy even without changes'
required: false
default: 'false'
type: boolean
target_branch:
description: 'Target branch for deployment'
required: false
default: 'main'
type: string
env:
NODE_VERSION: '18'
PNPM_VERSION: '8'
TARGET_REPO: 'Reifonas/TS_RDO'
jobs:
# Job para verificar mudanças
check-changes:
runs-on: ubuntu-latest
outputs:
has-changes: ${{ steps.changes.outputs.has-changes }}
changed-files: ${{ steps.changes.outputs.changed-files }}
steps:
- name: Checkout código
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Verificar mudanças
id: changes
run: |
if [ "${{ github.event_name }}" = "schedule" ] || [ "${{ inputs.force_deploy }}" = "true" ]; then
echo "has-changes=true" >> $GITHUB_OUTPUT
echo "changed-files=scheduled-run" >> $GITHUB_OUTPUT
else
CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD || echo "")
if [ -n "$CHANGED_FILES" ]; then
echo "has-changes=true" >> $GITHUB_OUTPUT
echo "changed-files<<EOF" >> $GITHUB_OUTPUT
echo "$CHANGED_FILES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
else
echo "has-changes=false" >> $GITHUB_OUTPUT
echo "changed-files=" >> $GITHUB_OUTPUT
fi
fi
# Job para build e testes
build-and-test:
needs: check-changes
if: needs.check-changes.outputs.has-changes == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout código
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- name: Instalar dependências
run: pnpm install --frozen-lockfile
- name: Executar linting
run: pnpm run lint
continue-on-error: true
- name: Executar testes
run: pnpm run test
continue-on-error: true
- name: Build do projeto
run: pnpm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-files
path: dist/
retention-days: 1
# Job para sincronização com repositório remoto
sync-to-remote:
needs: [check-changes, build-and-test]
if: needs.check-changes.outputs.has-changes == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout código atual
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
- name: Configurar Git
run: |
git config --global user.name "Auto Sync Bot"
git config --global user.email "auto-sync@rdo-app.com"
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-files
path: dist/
- name: Preparar arquivos para sincronização
run: |
# Criar diretório temporário
mkdir -p /tmp/sync-repo
# Copiar arquivos essenciais (excluir node_modules, logs, etc.)
rsync -av --exclude='node_modules' \
--exclude='.git' \
--exclude='logs' \
--exclude='.env.local' \
--exclude='*.log' \
--exclude='.trae' \
--exclude='android' \
--exclude='ios' \
./ /tmp/sync-repo/
- name: Clonar repositório remoto
env:
REMOTE_TOKEN: ${{ secrets.REMOTE_REPO_TOKEN }}
run: |
cd /tmp
git clone https://${REMOTE_TOKEN}@github.com/${{ env.TARGET_REPO }}.git remote-repo
cd remote-repo
git checkout ${{ inputs.target_branch || 'main' }} || git checkout -b ${{ inputs.target_branch || 'main' }}
- name: Sincronizar arquivos
run: |
cd /tmp/remote-repo
# Remover arquivos antigos (exceto .git)
find . -mindepth 1 -maxdepth 1 ! -name '.git' -exec rm -rf {} +
# Copiar novos arquivos
cp -r /tmp/sync-repo/* .
cp -r /tmp/sync-repo/.* . 2>/dev/null || true
# Adicionar todos os arquivos
git add .
- name: Commit e Push para repositório remoto
env:
REMOTE_TOKEN: ${{ secrets.REMOTE_REPO_TOKEN }}
run: |
cd /tmp/remote-repo
# Verificar se há mudanças
if git diff --staged --quiet; then
echo "Nenhuma mudança para sincronizar"
exit 0
fi
# Criar mensagem de commit
COMMIT_MSG="Auto-sync from RDO-C: $(date '+%Y-%m-%d %H:%M:%S')"
if [ "${{ needs.check-changes.outputs.changed-files }}" != "scheduled-run" ]; then
COMMIT_MSG="$COMMIT_MSG\n\nChanged files:\n${{ needs.check-changes.outputs.changed-files }}"
fi
# Commit e push
git commit -m "$COMMIT_MSG"
git push origin ${{ inputs.target_branch || 'main' }}
# Job para deploy (se necessário)
deploy:
needs: [check-changes, build-and-test, sync-to-remote]
if: needs.check-changes.outputs.has-changes == 'true' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Checkout código
uses: actions/checkout@v4
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-files
path: dist/
- name: Deploy para Netlify
uses: nwtgck/actions-netlify@v3.0
with:
publish-dir: './dist'
production-branch: main
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy-message: "Auto-deploy from commit ${{ github.sha }}"
enable-pull-request-comment: false
enable-commit-comment: true
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
timeout-minutes: 10
# Job para notificações
notify:
needs: [check-changes, build-and-test, sync-to-remote, deploy]
if: always() && needs.check-changes.outputs.has-changes == 'true'
runs-on: ubuntu-latest
steps:
- name: Notificar sucesso
if: needs.sync-to-remote.result == 'success'
run: |
echo "✅ Sincronização concluída com sucesso!"
echo "Repositório: ${{ env.TARGET_REPO }}"
echo "Branch: ${{ inputs.target_branch || 'main' }}"
echo "Commit: ${{ github.sha }}"
- name: Notificar falha
if: needs.sync-to-remote.result == 'failure' || needs.build-and-test.result == 'failure'
run: |
echo "❌ Falha na sincronização!"
echo "Verifique os logs para mais detalhes."
exit 1
# Job para limpeza
cleanup:
needs: [check-changes, build-and-test, sync-to-remote, deploy, notify]
if: always()
runs-on: ubuntu-latest
steps:
- name: Limpar artifacts
uses: actions/github-script@v7
with:
script: |
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.runId,
});
for (const artifact of artifacts.data.artifacts) {
if (artifact.name === 'build-files') {
await github.rest.actions.deleteArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: artifact.id,
});
console.log(`Artifact ${artifact.name} removido`);
}
}

View File

@@ -1,64 +0,0 @@
name: Deploy to GitHub Pages
on:
# Executa no push para a branch main
push:
branches: [ main ]
# Permite execução manual
workflow_dispatch:
# Define permissões necessárias para o GITHUB_TOKEN
permissions:
contents: read
pages: write
id-token: write
# Permite apenas um deploy por vez
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
# Job de build
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: latest
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm run build
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: './dist'
# Job de deploy
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

View File

@@ -1,352 +0,0 @@
# Análise Completa e Plano de Otimização - Aplicativo RDO
## 1. Resumo Executivo
Este documento apresenta uma análise detalhada da estrutura atual do aplicativo RDO (Relatório Diário de Obra) e propõe um plano abrangente de otimização, refatoração e modernização do código. O objetivo é melhorar a performance, legibilidade, manutenibilidade e seguir as melhores práticas atuais de desenvolvimento.
## 2. Análise da Arquitetura Atual
### 2.1 Pontos Fortes Identificados
- ✅ Uso de TypeScript para tipagem estática
- ✅ Arquitetura baseada em React 18 com hooks modernos
- ✅ Integração com Supabase para backend-as-a-service
- ✅ Implementação de React Query para gerenciamento de estado servidor
- ✅ Uso de Zustand para estado global
- ✅ Configuração de PWA com Capacitor
- ✅ Implementação de modo offline com Dexie
- ✅ Estrutura de pastas organizada
### 2.2 Problemas Críticos Identificados
#### 2.2.1 Duplicação de Lógica de Estado
-**Problema**: Existem dois sistemas paralelos para gerenciamento de obras:
- `src/hooks/useObras.ts` (useState + useEffect)
- `src/hooks/queries/useObras.ts` (React Query)
- `src/stores/useObraStore.ts` (Zustand)
-**Impacto**: Inconsistência de dados, complexidade desnecessária, bugs potenciais
#### 2.2.2 Configuração TypeScript Permissiva
-**Problema**: `tsconfig.json` com `strict: false` e outras verificações desabilitadas
-**Impacto**: Perda de benefícios da tipagem estática, bugs em runtime
#### 2.2.3 Estrutura de Rotas Repetitiva
-**Problema**: Código repetitivo no `App.tsx` com múltiplas rotas similares
-**Impacto**: Dificulta manutenção e adiciona verbosidade
#### 2.2.4 Falta de Padronização de Componentes
-**Problema**: Componentes sem padrão consistente de props e estrutura
-**Impacto**: Dificuldade de manutenção e reutilização
## 3. Plano de Otimização Detalhado
### 3.1 Fase 1: Consolidação da Arquitetura de Estado (Prioridade Alta)
#### 3.1.1 Migração para Arquitetura Unificada
**Objetivo**: Eliminar duplicações e criar uma única fonte de verdade
**Ações**:
1. **Manter apenas React Query + Zustand**:
- Remover hooks baseados em useState (`useObras.ts`, `useRdos.ts`, etc.)
- Usar React Query para estado servidor (dados do Supabase)
- Usar Zustand apenas para estado cliente (UI, configurações)
2. **Reestruturar hooks de queries**:
```typescript
// Estrutura otimizada
src/hooks/
├── queries/
│ ├── useObrasQuery.ts // React Query apenas
│ ├── useRdosQuery.ts // React Query apenas
│ └── useUsersQuery.ts // React Query apenas
├── stores/
│ ├── useUIStore.ts // Estado da UI
│ ├── useConfigStore.ts // Configurações
│ └── useOfflineStore.ts // Estado offline
└── index.ts
```
3. **Implementar padrão de custom hooks compostos**:
```typescript
// Exemplo: useObra.ts
export const useObra = (id: string) => {
const query = useObraQuery(id);
const mutation = useUpdateObraMutation();
const uiState = useUIStore();
return {
...query,
update: mutation.mutate,
isUpdating: mutation.isPending,
// Lógica composta
};
};
```
#### 3.1.2 Otimização do React Query
**Configurações aprimoradas**:
```typescript
// queryClient.ts otimizado
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutos
gcTime: 10 * 60 * 1000, // 10 minutos
retry: (failureCount, error) => {
if (error?.status === 401) return false;
return failureCount < 3;
},
refetchOnWindowFocus: false, // Otimização mobile
},
},
});
```
### 3.2 Fase 2: Modernização do TypeScript (Prioridade Alta)
#### 3.2.1 Configuração Strict
```json
// tsconfig.json otimizado
{
"compilerOptions": {
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"exactOptionalPropertyTypes": true,
"noUncheckedIndexedAccess": true
}
}
```
#### 3.2.2 Melhoria dos Tipos
1. **Criar tipos específicos do domínio**:
```typescript
// types/domain.ts
export type ObraStatus = 'ativa' | 'pausada' | 'concluida' | 'cancelada';
export type UserRole = 'admin' | 'engenheiro' | 'mestre_obra' | 'usuario';
export interface ObraWithRelations extends Obra {
responsavel?: Usuario;
rdos?: RDO[];
}
```
2. **Implementar branded types para IDs**:
```typescript
export type ObraId = string & { readonly brand: unique symbol };
export type UserId = string & { readonly brand: unique symbol };
```
### 3.3 Fase 3: Refatoração de Componentes (Prioridade Média)
#### 3.3.1 Sistema de Design Consistente
1. **Criar componentes base reutilizáveis**:
```typescript
// components/ui/
├── Button/
│ ├── Button.tsx
│ ├── Button.types.ts
│ └── Button.stories.tsx
├── Input/
├── Modal/
└── Card/
```
2. **Implementar compound components**:
```typescript
// Exemplo: Modal compound component
export const Modal = {
Root: ModalRoot,
Header: ModalHeader,
Body: ModalBody,
Footer: ModalFooter,
};
```
#### 3.3.2 Otimização de Performance
1. **Implementar React.memo estratégico**:
```typescript
export const ObraCard = React.memo(({ obra }: { obra: Obra }) => {
// Componente otimizado
});
```
2. **Usar React.lazy para code splitting**:
```typescript
const ObraDetails = React.lazy(() => import('./pages/ObraDetails'));
```
3. **Implementar virtualization para listas grandes**:
```typescript
import { FixedSizeList as List } from 'react-window';
```
### 3.4 Fase 4: Otimização de Rotas (Prioridade Média)
#### 3.4.1 Configuração Declarativa de Rotas
```typescript
// routes/config.ts
export const routes = [
{
path: '/',
element: Dashboard,
protected: true,
layout: MainLayout,
},
{
path: '/obra/:id',
element: ObraDetails,
protected: true,
layout: null, // Tela cheia
},
] as const;
// App.tsx simplificado
export default function App() {
return (
<Router>
<QueryProvider>
<AuthProvider>
<RouteRenderer routes={routes} />
</AuthProvider>
</QueryProvider>
</Router>
);
}
```
### 3.5 Fase 5: Otimizações de Performance (Prioridade Média)
#### 3.5.1 Bundle Optimization
1. **Configurar Vite para otimização**:
```typescript
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
supabase: ['@supabase/supabase-js'],
ui: ['@radix-ui/react-dialog', '@radix-ui/react-select'],
},
},
},
},
});
```
2. **Implementar preloading estratégico**:
```typescript
// Preload de rotas críticas
const prefetchObraDetails = (obraId: string) => {
queryClient.prefetchQuery({
queryKey: ['obra', obraId],
queryFn: () => getObra(obraId),
});
};
```
#### 3.5.2 Otimização de Imagens e Assets
1. **Implementar lazy loading de imagens**
2. **Usar WebP com fallback**
3. **Configurar service worker para cache de assets**
### 3.6 Fase 6: Melhorias de DX (Developer Experience)
#### 3.6.1 Ferramentas de Desenvolvimento
1. **Configurar ESLint mais rigoroso**:
```javascript
// eslint.config.js
export default [
...tseslint.configs.strictTypeChecked,
{
rules: {
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/prefer-nullish-coalescing': 'error',
'react-hooks/exhaustive-deps': 'error',
},
},
];
```
2. **Implementar Prettier com configuração consistente**
3. **Configurar Husky para pre-commit hooks**
#### 3.6.2 Testing Strategy
1. **Implementar testes unitários com Vitest**
2. **Testes de integração com Testing Library**
3. **E2E tests com Playwright**
## 4. Cronograma de Implementação
### Sprint 1 (2 semanas) - Fundação
- [ ] Configurar TypeScript strict mode
- [ ] Consolidar arquitetura de estado (Fase 1)
- [ ] Remover hooks duplicados
- [ ] Configurar ferramentas de desenvolvimento
### Sprint 2 (2 semanas) - Componentes
- [ ] Criar sistema de design base
- [ ] Refatorar componentes principais
- [ ] Implementar compound components
- [ ] Otimizar performance com React.memo
### Sprint 3 (1 semana) - Rotas e Performance
- [ ] Refatorar sistema de rotas
- [ ] Implementar code splitting
- [ ] Otimizar bundle com Vite
- [ ] Configurar preloading
### Sprint 4 (1 semana) - Finalização
- [ ] Testes e validação
- [ ] Documentação
- [ ] Deploy e monitoramento
## 5. Métricas de Sucesso
### 5.1 Performance
- **Bundle size**: Redução de 30%
- **First Contentful Paint**: < 1.5s
- **Largest Contentful Paint**: < 2.5s
- **Time to Interactive**: < 3s
### 5.2 Qualidade de Código
- **TypeScript coverage**: 100%
- **ESLint errors**: 0
- **Test coverage**: > 80%
- **Duplicação de código**: < 5%
### 5.3 Developer Experience
- **Build time**: Redução de 40%
- **Hot reload**: < 200ms
- **Type checking**: < 5s
## 6. Riscos e Mitigações
### 6.1 Riscos Identificados
1. **Breaking changes durante refatoração**
- *Mitigação*: Implementar testes abrangentes antes das mudanças
2. **Regressões de funcionalidade**
- *Mitigação*: Refatoração incremental com validação contínua
3. **Resistência da equipe às mudanças**
- *Mitigação*: Documentação clara e treinamento
### 6.2 Plano de Rollback
- Manter branches de feature para cada fase
- Implementar feature flags para mudanças críticas
- Monitoramento contínuo em produção
## 7. Conclusão
Este plano de otimização transformará o aplicativo RDO em uma aplicação moderna, performática e maintível. A implementação incremental garante baixo risco enquanto maximiza os benefícios de cada melhoria.
A consolidação da arquitetura de estado e a modernização do TypeScript são as prioridades mais altas, pois impactam diretamente na estabilidade e manutenibilidade do código.
Com a implementação completa deste plano, esperamos:
- **50% de redução** no tempo de desenvolvimento de novas features
- **30% de melhoria** na performance da aplicação
- **90% de redução** em bugs relacionados a tipos
- **Experiência de desenvolvimento** significativamente melhorada

File diff suppressed because it is too large Load Diff

View File

@@ -1,815 +0,0 @@
# Documento de Arquitetura Técnica - RDO Mobile App
## 1. Design da Arquitetura
```mermaid
graph TD
A[Usuário Mobile] --> B[React Native/PWA Frontend]
B --> C[Supabase SDK]
C --> D[Supabase Backend]
subgraph "Frontend Layer"
B
E[Zustand State Management]
F[React Hook Form]
G[Framer Motion]
H[Tailwind CSS]
end
subgraph "Backend as a Service (Supabase)"
D
I[PostgreSQL Database]
J[Authentication]
K[Storage (Fotos/Docs)]
L[Real-time Subscriptions]
end
subgraph "Funcionalidades Offline"
M[IndexedDB Cache]
N[Service Worker]
O[Background Sync]
end
B --> M
N --> B
O --> C
```
## 2. Descrição das Tecnologias
* **Frontend**: React\@18 + TypeScript + Vite + Tailwind CSS
* **Mobile**: PWA (Progressive Web App) com Capacitor para recursos nativos
* **Estado**: Zustand para gerenciamento de estado global
* **Formulários**: React Hook Form + Zod para validação
* **Animações**: Framer Motion para microinterações
* **UI Components**: Headless UI + Radix UI primitives
* **Ícones**: Phosphor Icons
* **Backend**: Supabase (PostgreSQL + Auth + Storage + Real-time)
* **Cache**: TanStack Query para cache de dados
* **Offline**: Workbox para service workers
## 3. Definições de Rotas
| Rota | Propósito |
| ----------------------- | --------------------------------------------- |
| / | Dashboard principal com visão geral das obras |
| /obra/:id | Detalhes específicos de uma obra |
| /obra/:id/rdo/novo | Formulário para criar novo RDO |
| /obra/:id/rdo/:rdoId | Visualizar/editar RDO específico |
| /obra/:id/tarefas | Lista de tarefas da obra |
| /cadastros | Menu principal de cadastros |
| /cadastros/obras | Formulário de cadastro de obras |
| /cadastros/usuarios | Gerenciamento de usuários |
| /cadastros/equipamentos | Cadastro de equipamentos |
| <br /> | <br /> |
| /cadastros/atividades | Tipos de atividades padrão |
| /relatorios | Dashboard de relatórios e exportações |
| /perfil | Configurações do usuário |
| /configuracoes | Configurações do aplicativo |
## 4. Definições de API
### 4.1 APIs Principais
**Autenticação de Usuário**
```
POST /auth/v1/token
```
Request:
| Nome do Parâmetro | Tipo | Obrigatório | Descrição |
| ----------------- | ------ | ----------- | ---------------- |
| email | string | true | Email do usuário |
| password | string | true | Senha do usuário |
Response:
| Nome do Parâmetro | Tipo | Descrição |
| ----------------- | ------ | ---------------------------- |
| access\_token | string | Token JWT para autenticação |
| user | object | Dados do usuário autenticado |
**Criar RDO**
```
POST /rest/v1/rdos
```
Request:
| Nome do Parâmetro | Tipo | Obrigatório | Descrição |
| --------------------- | ------ | ----------- | ------------------------------ |
| obra\_id | uuid | true | ID da obra |
| data\_relatorio | date | true | Data do relatório |
| condicoes\_climaticas | string | true | Condições do tempo |
| atividades | array | true | Lista de atividades executadas |
| mao\_de\_obra | array | false | Funcionários presentes |
| equipamentos | array | false | Equipamentos utilizados |
| ocorrencias | array | false | Ocorrências registradas |
Response:
| Nome do Parâmetro | Tipo | Descrição |
| ----------------- | --------- | ------------------- |
| id | uuid | ID do RDO criado |
| status | string | Status do relatório |
| created\_at | timestamp | Data de criação |
Exemplo:
```json
{
"obra_id": "123e4567-e89b-12d3-a456-426614174000",
"data_relatorio": "2024-01-15",
"condicoes_climaticas": "Ensolarado",
"atividades": [
{
"tipo": "Concretagem",
"descricao": "Concretagem da laje do 2º pavimento",
"percentual_concluido": 75
}
]
}
```
**Listar Obras**
```
GET /rest/v1/obras
```
Response:
| Nome do Parâmetro | Tipo | Descrição |
| ----------------- | ------ | ---------------------------------------- |
| id | uuid | ID da obra |
| nome | string | Nome da obra |
| endereco | string | Endereço da obra |
| status | string | Status atual (ativa, pausada, concluída) |
| progresso | number | Percentual de conclusão |
## 5. Arquitetura do Servidor
```mermaid
graph TD
A[Cliente Mobile] --> B[Supabase Edge Functions]
B --> C[Supabase Auth]
B --> D[PostgreSQL Database]
B --> E[Supabase Storage]
subgraph "Supabase Backend"
C
D
E
F[Real-time Engine]
G[Row Level Security]
end
subgraph "Edge Functions"
H[Geração de Relatórios PDF]
I[Processamento de Imagens]
J[Notificações Push]
end
B --> H
B --> I
B --> J
```
## 6. Modelo de Dados
### 6.1 Definição do Modelo de Dados
```mermaid
erDiagram
USUARIOS ||--o{ OBRAS : gerencia
USUARIOS ||--o{ RDOS : cria
OBRAS ||--o{ RDOS : possui
OBRAS ||--o{ TAREFAS : contem
RDOS ||--o{ RDO_ATIVIDADES : possui
RDOS ||--o{ RDO_MAO_OBRA : registra
RDOS ||--o{ RDO_EQUIPAMENTOS : utiliza
RDOS ||--o{ RDO_OCORRENCIAS : reporta
RDOS ||--o{ RDO_ANEXOS : contem
RDOS ||--o{ RDO_INSPECOES_SOLDA : possui
RDOS ||--o{ RDO_VERIFICACOES_TORQUE : possui
TAREFAS ||--o{ TASK_LOGS : possui
USUARIOS ||--o{ TASK_LOGS : executa
USUARIOS {
uuid id PK
string email UK
string nome
string telefone
string cargo
string role
boolean ativo
timestamp created_at
timestamp updated_at
}
OBRAS {
uuid id PK
string nome
string descricao
string endereco
string cep
string cidade
string estado
uuid responsavel_id FK
date data_inicio
date data_prevista_fim
decimal progresso_geral
string status
jsonb configuracoes
timestamp created_at
timestamp updated_at
}
RDOS {
uuid id PK
uuid obra_id FK
uuid criado_por FK
date data_relatorio
string condicoes_climaticas
text observacoes_gerais
string status
uuid aprovado_por FK
timestamp aprovado_em
timestamp created_at
timestamp updated_at
}
RDO_ATIVIDADES {
uuid id PK
uuid rdo_id FK
string tipo_atividade
text descricao
string localizacao
decimal percentual_concluido
integer ordem
timestamp created_at
}
RDO_MAO_OBRA {
uuid id PK
uuid rdo_id FK
string funcao
integer quantidade
decimal horas_trabalhadas
text observacoes
timestamp created_at
}
RDO_EQUIPAMENTOS {
uuid id PK
uuid rdo_id FK
string nome_equipamento
string tipo
decimal horas_utilizadas
decimal combustivel_gasto
text observacoes
timestamp created_at
}
RDO_OCORRENCIAS {
uuid id PK
uuid rdo_id FK
string tipo_ocorrencia
text descricao
string gravidade
text acao_tomada
timestamp created_at
}
RDO_ANEXOS {
uuid id PK
uuid rdo_id FK
string nome_arquivo
string tipo_arquivo
string url_storage
integer tamanho_bytes
text descricao
timestamp created_at
}
RDO_INSPECOES_SOLDA {
uuid id PK
uuid rdo_id FK
string identificacao_junta
string status_inspecao
string metodo_inspecao
text observacoes
uuid inspecionado_por FK
timestamp created_at
}
RDO_VERIFICACOES_TORQUE {
uuid id PK
uuid rdo_id FK
string identificacao_parafuso
decimal torque_especificado
decimal torque_aplicado
string status_verificacao
text observacoes
uuid verificado_por FK
timestamp created_at
}
TAREFAS {
uuid id PK
uuid obra_id FK
string titulo
text descricao
string status
string prioridade
uuid responsavel_id FK
date data_inicio
date data_fim
decimal progresso
jsonb metadados
timestamp created_at
timestamp updated_at
}
TASK_LOGS {
uuid id PK
uuid task_id FK
uuid usuario_id FK
string tipo_evento
text descricao
jsonb detalhes
timestamp created_at
}
RDOS ||--o{ OCORRENCIAS : possui
RDOS ||--o{ ANEXOS : contem
OBRAS ||--o{ TAREFAS : possui
EQUIPAMENTOS ||--o{ EQUIPAMENTOS_UTILIZADOS : referencia
USUARIOS {
uuid id PK
string email
string nome
string funcao
string telefone
timestamp created_at
timestamp updated_at
}
OBRAS {
uuid id PK
string nome
string endereco
string descricao
date data_inicio
date data_prevista_fim
string status
decimal progresso_percentual
uuid responsavel_id FK
timestamp created_at
}
RDOS {
uuid id PK
uuid obra_id FK
uuid criado_por FK
date data_relatorio
string condicoes_climaticas
text observacoes_gerais
string status
timestamp created_at
}
ATIVIDADES {
uuid id PK
uuid rdo_id FK
string tipo_atividade
text descricao
string localizacao
decimal percentual_concluido
time hora_inicio
time hora_fim
}
MAO_DE_OBRA {
uuid id PK
uuid rdo_id FK
string nome_funcionario
string funcao
decimal horas_trabalhadas
boolean presente
}
EQUIPAMENTOS {
uuid id PK
string nome
string tipo
string modelo
string status
text observacoes
}
EQUIPAMENTOS_UTILIZADOS {
uuid id PK
uuid rdo_id FK
uuid equipamento_id FK
decimal horas_operacao
decimal combustivel_consumido
text observacoes
}
OCORRENCIAS {
uuid id PK
uuid rdo_id FK
string tipo
text descricao
string gravidade
boolean resolvida
}
ANEXOS {
uuid id PK
uuid rdo_id FK
string tipo
string url_arquivo
string nome_arquivo
text descricao
}
TAREFAS {
uuid id PK
uuid obra_id FK
string titulo
text descricao
date data_prevista
string status
string prioridade
uuid responsavel_id FK
}
```
### 6.2 Linguagem de Definição de Dados (DDL)
**Tabela de Usuários (usuarios)**
```sql
-- Criar tabela
CREATE TABLE usuarios (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
nome VARCHAR(100) NOT NULL,
telefone VARCHAR(20),
cargo VARCHAR(100),
role VARCHAR(20) DEFAULT 'user' CHECK (role IN ('admin', 'supervisor', 'user')),
ativo BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Criar índices
CREATE INDEX idx_usuarios_email ON usuarios(email);
CREATE INDEX idx_usuarios_role ON usuarios(role);
CREATE INDEX idx_usuarios_ativo ON usuarios(ativo);
-- Dados iniciais
INSERT INTO usuarios (email, nome, cargo, role) VALUES
('admin@rdo.com', 'Administrador', 'Gerente de Projeto', 'admin'),
('supervisor@rdo.com', 'Supervisor', 'Supervisor de Obra', 'supervisor'),
('user@rdo.com', 'Usuário Padrão', 'Técnico', 'user');
```
**Tabela de Obras (obras)**
```sql
CREATE TABLE obras (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
nome VARCHAR(255) NOT NULL,
descricao TEXT,
endereco TEXT,
cep VARCHAR(10),
cidade VARCHAR(100),
estado VARCHAR(2),
responsavel_id UUID REFERENCES usuarios(id),
data_inicio DATE,
data_prevista_fim DATE,
progresso_geral DECIMAL(5,2) DEFAULT 0.00,
status VARCHAR(20) DEFAULT 'planejamento' CHECK (status IN ('planejamento', 'em_andamento', 'pausada', 'concluida', 'cancelada')),
configuracoes JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_obras_responsavel ON obras(responsavel_id);
CREATE INDEX idx_obras_status ON obras(status);
CREATE INDEX idx_obras_data_inicio ON obras(data_inicio);
```
**Tabela de RDOs (rdos)**
```sql
CREATE TABLE rdos (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
obra_id UUID NOT NULL REFERENCES obras(id),
criado_por UUID NOT NULL REFERENCES usuarios(id),
data_relatorio DATE NOT NULL,
condicoes_climaticas VARCHAR(100),
observacoes_gerais TEXT,
status VARCHAR(20) DEFAULT 'rascunho' CHECK (status IN ('rascunho', 'enviado', 'aprovado', 'rejeitado')),
aprovado_por UUID REFERENCES usuarios(id),
aprovado_em TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_rdos_obra ON rdos(obra_id);
CREATE INDEX idx_rdos_criado_por ON rdos(criado_por);
CREATE INDEX idx_rdos_data_relatorio ON rdos(data_relatorio);
CREATE INDEX idx_rdos_status ON rdos(status);
```
**Tabela de Atividades do RDO (rdo_atividades)**
```sql
CREATE TABLE rdo_atividades (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
rdo_id UUID NOT NULL REFERENCES rdos(id) ON DELETE CASCADE,
tipo_atividade VARCHAR(100) NOT NULL,
descricao TEXT,
localizacao VARCHAR(255),
percentual_concluido DECIMAL(5,2) DEFAULT 0.00,
ordem INTEGER DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_rdo_atividades_rdo ON rdo_atividades(rdo_id);
CREATE INDEX idx_rdo_atividades_tipo ON rdo_atividades(tipo_atividade);
```
**Tabela de Mão de Obra (rdo_mao_obra)**
```sql
CREATE TABLE rdo_mao_obra (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
rdo_id UUID NOT NULL REFERENCES rdos(id) ON DELETE CASCADE,
funcao VARCHAR(100) NOT NULL,
quantidade INTEGER NOT NULL DEFAULT 1,
horas_trabalhadas DECIMAL(5,2) NOT NULL DEFAULT 0.00,
observacoes TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_rdo_mao_obra_rdo ON rdo_mao_obra(rdo_id);
CREATE INDEX idx_rdo_mao_obra_funcao ON rdo_mao_obra(funcao);
```
**Tabela de Equipamentos (rdo_equipamentos)**
```sql
CREATE TABLE rdo_equipamentos (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
rdo_id UUID NOT NULL REFERENCES rdos(id) ON DELETE CASCADE,
nome_equipamento VARCHAR(255) NOT NULL,
tipo VARCHAR(100),
horas_utilizadas DECIMAL(5,2) DEFAULT 0.00,
combustivel_gasto DECIMAL(8,2) DEFAULT 0.00,
observacoes TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_rdo_equipamentos_rdo ON rdo_equipamentos(rdo_id);
CREATE INDEX idx_rdo_equipamentos_tipo ON rdo_equipamentos(tipo);
```
**Tabela de Ocorrências (rdo_ocorrencias)**
```sql
CREATE TABLE rdo_ocorrencias (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
rdo_id UUID NOT NULL REFERENCES rdos(id) ON DELETE CASCADE,
tipo_ocorrencia VARCHAR(100) NOT NULL,
descricao TEXT NOT NULL,
gravidade VARCHAR(20) DEFAULT 'baixa' CHECK (gravidade IN ('baixa', 'media', 'alta', 'critica')),
acao_tomada TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_rdo_ocorrencias_rdo ON rdo_ocorrencias(rdo_id);
CREATE INDEX idx_rdo_ocorrencias_tipo ON rdo_ocorrencias(tipo_ocorrencia);
CREATE INDEX idx_rdo_ocorrencias_gravidade ON rdo_ocorrencias(gravidade);
```
**Tabela de Anexos (rdo_anexos)**
```sql
CREATE TABLE rdo_anexos (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
rdo_id UUID NOT NULL REFERENCES rdos(id) ON DELETE CASCADE,
nome_arquivo VARCHAR(255) NOT NULL,
tipo_arquivo VARCHAR(50),
url_storage TEXT NOT NULL,
tamanho_bytes INTEGER,
descricao TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_rdo_anexos_rdo ON rdo_anexos(rdo_id);
CREATE INDEX idx_rdo_anexos_tipo ON rdo_anexos(tipo_arquivo);
```
**Tabela de Inspeções de Solda (rdo_inspecoes_solda)**
```sql
CREATE TABLE rdo_inspecoes_solda (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
rdo_id UUID NOT NULL REFERENCES rdos(id) ON DELETE CASCADE,
identificacao_junta VARCHAR(100) NOT NULL,
status_inspecao VARCHAR(20) DEFAULT 'pendente' CHECK (status_inspecao IN ('pendente', 'aprovada', 'rejeitada', 'retrabalho')),
metodo_inspecao VARCHAR(100),
observacoes TEXT,
inspecionado_por UUID REFERENCES usuarios(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_rdo_inspecoes_solda_rdo ON rdo_inspecoes_solda(rdo_id);
CREATE INDEX idx_rdo_inspecoes_solda_status ON rdo_inspecoes_solda(status_inspecao);
```
**Tabela de Verificações de Torque (rdo_verificacoes_torque)**
```sql
CREATE TABLE rdo_verificacoes_torque (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
rdo_id UUID NOT NULL REFERENCES rdos(id) ON DELETE CASCADE,
identificacao_parafuso VARCHAR(100) NOT NULL,
torque_especificado DECIMAL(8,2) NOT NULL,
torque_aplicado DECIMAL(8,2) NOT NULL,
status_verificacao VARCHAR(20) DEFAULT 'conforme' CHECK (status_verificacao IN ('conforme', 'nao_conforme', 'retrabalho')),
observacoes TEXT,
verificado_por UUID REFERENCES usuarios(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_rdo_verificacoes_torque_rdo ON rdo_verificacoes_torque(rdo_id);
CREATE INDEX idx_rdo_verificacoes_torque_status ON rdo_verificacoes_torque(status_verificacao);
```
**Tabela de Tarefas (tarefas)**
```sql
CREATE TABLE tarefas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
obra_id UUID NOT NULL REFERENCES obras(id),
titulo VARCHAR(255) NOT NULL,
descricao TEXT,
status VARCHAR(20) DEFAULT 'pendente' CHECK (status IN ('pendente', 'em_andamento', 'concluida', 'cancelada')),
prioridade VARCHAR(20) DEFAULT 'media' CHECK (prioridade IN ('baixa', 'media', 'alta', 'urgente')),
responsavel_id UUID REFERENCES usuarios(id),
data_inicio DATE,
data_fim DATE,
progresso DECIMAL(5,2) DEFAULT 0.00,
metadados JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_tarefas_obra ON tarefas(obra_id);
CREATE INDEX idx_tarefas_responsavel ON tarefas(responsavel_id);
CREATE INDEX idx_tarefas_status ON tarefas(status);
CREATE INDEX idx_tarefas_prioridade ON tarefas(prioridade);
```
**Tabela de Logs de Tarefas (task_logs)**
```sql
CREATE TABLE task_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
task_id UUID NOT NULL REFERENCES tarefas(id) ON DELETE CASCADE,
usuario_id UUID NOT NULL REFERENCES usuarios(id),
tipo_evento VARCHAR(50) NOT NULL,
descricao TEXT,
detalhes JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_task_logs_task ON task_logs(task_id);
CREATE INDEX idx_task_logs_usuario ON task_logs(usuario_id);
CREATE INDEX idx_task_logs_tipo ON task_logs(tipo_evento);
CREATE INDEX idx_task_logs_created_at ON task_logs(created_at DESC);
```
**Tabela de Obras**
```sql
-- Criar tabela de obras
CREATE TABLE obras (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
nome VARCHAR(200) NOT NULL,
endereco TEXT NOT NULL,
descricao TEXT,
data_inicio DATE NOT NULL,
data_prevista_fim DATE,
status VARCHAR(20) DEFAULT 'ativa' CHECK (status IN ('ativa', 'pausada', 'concluida')),
progresso_percentual DECIMAL(5,2) DEFAULT 0.00,
responsavel_id UUID REFERENCES usuarios(id),
orcamento_total DECIMAL(15,2),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Criar índices
CREATE INDEX idx_obras_status ON obras(status);
CREATE INDEX idx_obras_responsavel ON obras(responsavel_id);
CREATE INDEX idx_obras_data_inicio ON obras(data_inicio DESC);
-- Políticas RLS
ALTER TABLE obras ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Usuários podem ver obras que participam" ON obras
FOR SELECT USING (
responsavel_id = auth.uid() OR
EXISTS (
SELECT 1 FROM usuarios
WHERE id = auth.uid() AND funcao IN ('Gestor', 'Engenheiro')
)
);
-- Permissões
GRANT SELECT ON obras TO anon;
GRANT ALL PRIVILEGES ON obras TO authenticated;
```
**Tabela de RDOs**
```sql
-- Criar tabela de RDOs
CREATE TABLE rdos (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
obra_id UUID NOT NULL REFERENCES obras(id) ON DELETE CASCADE,
criado_por UUID NOT NULL REFERENCES usuarios(id),
data_relatorio DATE NOT NULL,
condicoes_climaticas VARCHAR(50) NOT NULL,
temperatura_min DECIMAL(4,1),
temperatura_max DECIMAL(4,1),
observacoes_gerais TEXT,
status VARCHAR(20) DEFAULT 'rascunho' CHECK (status IN ('rascunho', 'enviado', 'aprovado')),
aprovado_por UUID REFERENCES usuarios(id),
aprovado_em TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Criar índices
CREATE INDEX idx_rdos_obra_id ON rdos(obra_id);
CREATE INDEX idx_rdos_data_relatorio ON rdos(data_relatorio DESC);
CREATE INDEX idx_rdos_status ON rdos(status);
CREATE INDEX idx_rdos_criado_por ON rdos(criado_por);
-- Constraint única para evitar múltiplos RDOs na mesma data/obra
CREATE UNIQUE INDEX idx_rdos_obra_data_unique ON rdos(obra_id, data_relatorio);
-- Políticas RLS
ALTER TABLE rdos ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Usuários podem ver RDOs das suas obras" ON rdos
FOR SELECT USING (
EXISTS (
SELECT 1 FROM obras
WHERE id = obra_id AND (
responsavel_id = auth.uid() OR
EXISTS (
SELECT 1 FROM usuarios
WHERE usuarios.id = auth.uid() AND funcao IN ('Gestor', 'Engenheiro')
)
)
)
);
-- Permissões
GRANT SELECT ON rdos TO anon;
GRANT ALL PRIVILEGES ON rdos TO authenticated;
```
**Dados Iniciais**
```sql
-- Inserir tipos de atividades padrão
INSERT INTO tipos_atividades (nome, categoria) VALUES
('Escavação', 'Terraplanagem'),
('Fundação', 'Estrutura'),
('Concretagem', 'Estrutura'),
('Alvenaria', 'Vedação'),
('Instalação Elétrica', 'Instalações'),
('Instalação Hidráulica', 'Instalações'),
('Revestimento', 'Acabamento'),
('Pintura', 'Acabamento');
-- Inserir condições climáticas padrão
INSERT INTO condicoes_climaticas (descricao) VALUES
('Ensolarado'),
('Parcialmente Nublado'),
('Nublado'),
('Chuvisco'),
('Chuva Leve'),
('Chuva Forte'),
('Tempestade');
```

View File

@@ -1,129 +0,0 @@
# Documento de Requisitos do Produto - App RDO Mobile
## 1. Visão Geral do Produto
O **RDO Mobile** é um aplicativo mobile-first para registro e acompanhamento diário de atividades em obras de construção civil. O app facilita o processo de criação de Relatórios Diários de Obra (RDO), permitindo que engenheiros, mestres de obra e gestores registrem atividades, mão de obra, equipamentos, condições climáticas e ocorrências de forma prática e organizada.
O produto visa substituir os relatórios em papel ou Excel por uma solução digital moderna, melhorando a comunicação entre canteiro de obra, escritório e clientes, proporcionando acompanhamento em tempo real do progresso das obras.
## 2. Funcionalidades Principais
### 2.1 Papéis de Usuário
| Papel | Método de Registro | Permissões Principais |
|-------|-------------------|----------------------|
| Mestre de Obra | Cadastro por convite | Criar e editar RDOs, registrar atividades, gerenciar mão de obra |
| Engenheiro | Cadastro por convite | Todas as permissões + aprovar RDOs, gerar relatórios |
| Gestor | Cadastro administrativo | Visualizar todas as obras, relatórios consolidados, gerenciar usuários |
| Cliente | Acesso por convite | Visualizar progresso da obra, relatórios aprovados |
### 2.2 Módulos Funcionais
Nosso aplicativo RDO Mobile consiste nas seguintes páginas principais:
1. **Dashboard Principal**: visão geral das obras, indicadores de progresso, últimas atividades
2. **Detalhes da Obra**: informações específicas da obra, histórico de RDOs, galeria de fotos
3. **Criar RDO**: formulário completo para registro diário de atividades
4. **Lista de Tarefas**: gerenciamento de atividades planejadas e executadas
5. **Cadastros**: formulários para obras, usuários, equipamentos e tipos de atividades
6. **Relatórios**: visualização e exportação de relatórios consolidados
### 2.3 Detalhes das Páginas
| Nome da Página | Nome do Módulo | Descrição da Funcionalidade |
|----------------|----------------|-----------------------------|
| Dashboard Principal | Visão Geral | Exibir cards das obras ativas, indicadores de progresso, últimos RDOs criados, notificações importantes |
| Dashboard Principal | Navegação Rápida | Acesso rápido para criar novo RDO, visualizar obras, acessar cadastros |
| Detalhes da Obra | Informações da Obra | Mostrar dados da obra, cronograma, responsáveis, localização |
| Detalhes da Obra | Histórico RDO | Listar RDOs anteriores com filtros por data, status, responsável |
| Detalhes da Obra | Galeria de Fotos | Visualizar fotos organizadas por data, com zoom e compartilhamento |
| Criar RDO | Informações Básicas | Campos para data, obra, responsável, condições climáticas |
| Criar RDO | Atividades Executadas | Adicionar atividades com descrição, localização, percentual concluído |
| Criar RDO | Mão de Obra | Registrar funcionários presentes, horas trabalhadas, função |
| Criar RDO | Equipamentos | Listar equipamentos utilizados, horas de operação, combustível |
| Criar RDO | Ocorrências | Registrar problemas, acidentes, atrasos com descrição detalhada |
| Criar RDO | Anexos | Adicionar fotos, documentos, assinaturas digitais |
| Lista de Tarefas | Tarefas Planejadas | Visualizar atividades programadas para o dia/semana |
| Lista de Tarefas | Controle de Execução | Marcar tarefas como iniciadas, em andamento, concluídas |
| Lista de Tarefas | Progresso Visual | Mostrar percentual de conclusão com barras de progresso |
| Cadastros | Cadastro de Obras | Formulário com dados da obra, endereço, responsáveis, cronograma |
| Cadastros | Cadastro de Usuários | Registrar funcionários com foto, função, permissões |
| Cadastros | Cadastro de Equipamentos | Listar equipamentos com especificações, manutenção, disponibilidade |
| Cadastros | Tipos de Atividades | Definir categorias de atividades padrão para seleção rápida |
| Relatórios | Relatórios Consolidados | Gerar relatórios por período, obra, tipo de atividade |
| Relatórios | Exportação | Exportar relatórios em PDF, Excel, compartilhar por email/WhatsApp |
## 3. Fluxo Principal
**Fluxo do Mestre de Obra:**
1. Acessa o dashboard e visualiza as obras sob sua responsabilidade
2. Seleciona uma obra específica para trabalhar
3. Cria um novo RDO diário preenchendo todas as seções obrigatórias
4. Adiciona fotos e registra ocorrências se necessário
5. Submete o RDO para aprovação do engenheiro
6. Acompanha o status das tarefas planejadas
**Fluxo do Engenheiro:**
1. Revisa RDOs pendentes de aprovação
2. Analisa o progresso geral das obras no dashboard
3. Gera relatórios consolidados para apresentação
4. Gerencia cadastros de equipamentos e atividades
```mermaid
graph TD
A[Dashboard Principal] --> B[Selecionar Obra]
B --> C[Detalhes da Obra]
C --> D[Criar Novo RDO]
C --> E[Lista de Tarefas]
C --> F[Histórico RDOs]
D --> G[Preencher Atividades]
G --> H[Registrar Mão de Obra]
H --> I[Adicionar Equipamentos]
I --> J[Registrar Ocorrências]
J --> K[Anexar Fotos]
K --> L[Submeter RDO]
A --> M[Cadastros]
M --> N[Obras]
M --> O[Usuários]
M --> P[Equipamentos]
A --> Q[Relatórios]
```
## 4. Design da Interface do Usuário
### 4.1 Estilo de Design
- **Cores Primárias**: Azul #2563EB (confiança, profissionalismo), Laranja #F97316 (energia, ação)
- **Cores Secundárias**: Cinza #64748B (neutro), Verde #10B981 (sucesso), Vermelho #EF4444 (alertas)
- **Estilo dos Botões**: Neumorphism com bordas arredondadas (12px), sombras sutis, efeitos de pressão
- **Tipografia**: Inter (títulos 18-24px), Open Sans (corpo 14-16px), peso regular e semi-bold
- **Layout**: Cards com Glassmorphism, navegação bottom tab, espaçamentos de 16px/24px
- **Ícones**: Phosphor Icons com estilo outline, tamanho 24px para ações principais
### 4.2 Visão Geral do Design das Páginas
| Nome da Página | Nome do Módulo | Elementos da UI |
|----------------|----------------|----------------|
| Dashboard Principal | Cards de Obras | Cards com glassmorphism, gradiente sutil azul-roxo, sombra 0 4px 20px rgba(0,0,0,0.1), bordas arredondadas 16px |
| Dashboard Principal | Indicadores | Gráficos circulares com animação, cores verde/amarelo/vermelho para status, fonte Inter 14px |
| Dashboard Principal | Navegação | Bottom navigation com 5 tabs, ícones Phosphor, background blur, altura 80px |
| Criar RDO | Formulário | Floating labels com animação, campos com border-radius 12px, validação em tempo real com cores |
| Criar RDO | Seções Expansíveis | Accordion com ícones de seta, transição suave 300ms, background rgba(255,255,255,0.1) |
| Lista de Tarefas | Cards de Tarefa | Swipe actions (verde para concluir, vermelho para excluir), checkbox animado, progress bar |
| Lista de Tarefas | Filtros | Chips com seleção múltipla, cores Material Design 3, espaçamento 8px |
| Cadastros | Formulários | Stepper horizontal, campos agrupados em cards, botões flutuantes para ações |
| Relatórios | Gráficos | Charts.js com tema dark/light, cores consistentes, animações de entrada |
### 4.3 Responsividade
**Mobile-first** com adaptação para tablets. Layouts flexíveis usando CSS Grid e Flexbox, tamanhos em rem (base 16px), espaçamentos escaláveis (16px/24px/32px). Safe areas para dispositivos com notch/dynamic island. Suporte a orientação portrait e landscape para tablets.
**Gestos de Navegação:**
- Swipe para direita: voltar à tela anterior
- Swipe para baixo: pull-to-refresh nas listas
- Swipe horizontal em cards: ações rápidas (editar/excluir)
- Long press: menu contextual
- Pinch to zoom: galeria de fotos
**Dark Mode Automático:**
Detecção da preferência do sistema com toggle manual. Paleta adaptada: backgrounds #1F2937, cards #374151, textos #F9FAFB, acentos mantidos com ajuste de saturação.

View File

@@ -1 +0,0 @@
{"projectId":"prj_yIejbjdWxs5ZZ5YWOEHm8UAqDS8k","orgId":"team_KY59uhtbyLyWDtIuz7Ew3uEk","projectName":"trae_42fqthkv","neverMindDeployCard":true}

View File

@@ -1,7 +1,51 @@
# Dependencies
node_modules
.pnp
.pnp.js
# Testing
coverage
# Production
build
dist
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# IDE
.vscode
.idea
*.swp
*.swo
*~
# Capacitor
ios/
android/
# Scripts
scripts/
# Documentation
documentation/
.trae/
# Database scripts
database_scripts/
*.sql
# Git
.git
.trae
.log
.figma
.gitignore
.gitattributes

8
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,8 @@
{
"css.lint.unknownAtRules": "ignore",
"scss.lint.unknownAtRules": "ignore",
"less.lint.unknownAtRules": "ignore",
"tailwindCSS.lint.unknownAtRules": "ignore",
"css.validate": false,
"scss.validate": false
}

View File

@@ -1,162 +0,0 @@
# ✅ CHECKLIST NETLIFY - CONFIGURAÇÃO COMPLETA
## STATUS ATUAL
- ✅ SQL executado com sucesso (2 usuários no banco)
- ✅ Políticas RLS configuradas
- ⚠️ Login ainda não funciona no Netlify
---
## 🔧 CONFIGURAÇÕES NECESSÁRIAS
### 1⃣ VARIÁVEIS DE AMBIENTE NO NETLIFY
**CRÍTICO:** O Netlify NÃO lê o arquivo `.env` local!
#### Como configurar:
1. Acesse: https://app.netlify.com
2. Clique no seu site
3. Vá em: **Site settings****Environment variables**
4. Clique em **Add a variable**
5. Adicione estas 2 variáveis:
```
Key: VITE_SUPABASE_URL
Value: https://xzudfhifaancyxxfdejx.supabase.co
Key: VITE_SUPABASE_ANON_KEY
Value: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inh6dWRmaGlmYWFuY3l4eGZkZWp4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzEzNjE0MTAsImV4cCI6MjA4NjkzNzQxMH0.c5CHWhfXcMrm27LfxEt6OZtttXXvVJOeWu-IbnNLfWY
```
6. **IMPORTANTE:** Após adicionar, clique em **Trigger deploy** para fazer um novo deploy
---
### 2⃣ REDIRECT URLs NO SUPABASE
1. Acesse: https://supabase.com/dashboard/project/xzudfhifaancyxxfdejx/auth/url-configuration
2. Na seção **Redirect URLs**, adicione (substitua `SEU-SITE` pelo domínio real):
```
https://SEU-SITE.netlify.app/auth/callback
https://SEU-SITE.netlify.app/*
```
**Exemplo:**
```
https://rdo-tracksteel.netlify.app/auth/callback
https://rdo-tracksteel.netlify.app/*
```
3. Na seção **Site URL**, configure:
```
https://SEU-SITE.netlify.app
```
4. Clique em **Save**
---
### 3⃣ GOOGLE CLOUD CONSOLE (OAuth)
1. Acesse: https://console.cloud.google.com/apis/credentials
2. Clique no seu **OAuth 2.0 Client ID**
3. Em **Authorized redirect URIs**, adicione:
```
https://xzudfhifaancyxxfdejx.supabase.co/auth/v1/callback
https://SEU-SITE.netlify.app/auth/callback
```
4. Clique em **Save**
---
## 🧪 COMO TESTAR
### Teste 1: Verificar Variáveis de Ambiente
Após fazer o deploy no Netlify:
1. Abra o site no Netlify
2. Pressione **F12** (Console do navegador)
3. Digite:
```javascript
console.log('URL:', import.meta.env.VITE_SUPABASE_URL)
console.log('Key:', import.meta.env.VITE_SUPABASE_ANON_KEY?.substring(0, 20) + '...')
```
**Resultado esperado:**
```
URL: https://xzudfhifaancyxxfdejx.supabase.co
Key: eyJhbGciOiJIUzI1NiIs...
```
**Se retornar `undefined`** = Variáveis não configuradas no Netlify!
---
### Teste 2: Verificar Login
1. Clique em "Login com Google"
2. Autorize o app
3. Deve redirecionar para `/auth/callback`
4. Deve fazer login com sucesso
---
## 🔍 DIAGNÓSTICO DE ERROS
### Erro: "Invalid redirect URL"
- ❌ Faltou adicionar URL no Supabase (passo 2)
- ❌ Faltou adicionar URI no Google Cloud (passo 3)
### Erro: "Environment variables not defined"
- ❌ Faltou configurar variáveis no Netlify (passo 1)
- ❌ Faltou fazer novo deploy após configurar
### Erro: 401 Unauthorized
- ✅ Já resolvido com o SQL que você executou!
### Login funciona mas volta para tela de login
- ❌ Variáveis de ambiente não configuradas
- ❌ Redirect URLs não configuradas
---
## 📋 ORDEM DE EXECUÇÃO
1.**SQL executado** (você já fez!)
2.**Configurar variáveis no Netlify** (passo 1)
3.**Fazer novo deploy no Netlify**
4.**Configurar Redirect URLs no Supabase** (passo 2)
5.**Configurar OAuth no Google Cloud** (passo 3)
6.**Testar login**
---
## 🎯 QUAL É O SEU DOMÍNIO NETLIFY?
Para eu te dar os comandos exatos, me informe:
**Qual é a URL do seu site no Netlify?**
Exemplo: `https://rdo-tracksteel.netlify.app`
Com essa informação, posso te dar os valores exatos para copiar e colar!
---
## 💡 DICA RÁPIDA
Se você ainda não sabe a URL do Netlify:
1. Acesse: https://app.netlify.com
2. Clique no seu site
3. A URL está no topo da página (ex: `https://nome-do-site.netlify.app`)

View File

@@ -1,172 +0,0 @@
# 🎯 CONFIGURAÇÃO EXATA - rdo.tracksteel.com.br
## ✅ VALORES PRONTOS PARA COPIAR E COLAR
---
## 1⃣ NETLIFY - VARIÁVEIS DE AMBIENTE
### Acesse:
https://app.netlify.com → Seu Site → Site settings → Environment variables
### Adicione estas 2 variáveis:
**Variável 1:**
```
Key: VITE_SUPABASE_URL
Value: https://xzudfhifaancyxxfdejx.supabase.co
```
**Variável 2:**
```
Key: VITE_SUPABASE_ANON_KEY
Value: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inh6dWRmaGlmYWFuY3l4eGZkZWp4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzEzNjE0MTAsImV4cCI6MjA4NjkzNzQxMH0.c5CHWhfXcMrm27LfxEt6OZtttXXvVJOeWu-IbnNLfWY
```
### ⚠️ IMPORTANTE:
Após adicionar as variáveis, clique em **"Trigger deploy"** para fazer um novo deploy!
---
## 2⃣ SUPABASE - REDIRECT URLs
### Acesse:
https://supabase.com/dashboard/project/xzudfhifaancyxxfdejx/auth/url-configuration
### Na seção "Redirect URLs", adicione estas URLs (uma por linha):
```
https://rdo.tracksteel.com.br/auth/callback
https://rdo.tracksteel.com.br/*
http://localhost:5173/auth/callback
http://localhost:3000/auth/callback
```
### Na seção "Site URL", configure:
```
https://rdo.tracksteel.com.br
```
### Clique em **Save**
---
## 3⃣ GOOGLE CLOUD CONSOLE - OAUTH
### Acesse:
https://console.cloud.google.com/apis/credentials
### Clique no seu OAuth 2.0 Client ID
### Na seção "Authorized redirect URIs", adicione estas URIs:
```
https://xzudfhifaancyxxfdejx.supabase.co/auth/v1/callback
https://rdo.tracksteel.com.br/auth/callback
http://localhost:5173/auth/callback
http://localhost:3000/auth/callback
```
### Clique em **Save**
---
## 4⃣ TESTE APÓS CONFIGURAR
### Teste 1: Verificar Variáveis de Ambiente
1. Abra: https://rdo.tracksteel.com.br
2. Pressione **F12** (Console do navegador)
3. Cole este código:
```javascript
console.log('URL:', import.meta.env.VITE_SUPABASE_URL)
console.log('Key:', import.meta.env.VITE_SUPABASE_ANON_KEY?.substring(0, 30) + '...')
```
**Resultado esperado:**
```
URL: https://xzudfhifaancyxxfdejx.supabase.co
Key: eyJhbGciOiJIUzI1NiIsInR5cCI6Ik...
```
**Se retornar `undefined`:**
- ❌ Variáveis não foram configuradas no Netlify
- ❌ Ou você não fez o novo deploy após configurar
---
### Teste 2: Login com Google
1. Acesse: https://rdo.tracksteel.com.br
2. Clique em **"Login com Google"**
3. Autorize o app
4. Deve redirecionar para: `https://rdo.tracksteel.com.br/auth/callback`
5. Deve fazer login com sucesso e ir para a página inicial
---
## 📋 CHECKLIST DE EXECUÇÃO
- [ ] 1. Configurar variáveis no Netlify
- [ ] 2. Fazer novo deploy no Netlify (Trigger deploy)
- [ ] 3. Aguardar deploy finalizar (2-3 minutos)
- [ ] 4. Configurar Redirect URLs no Supabase
- [ ] 5. Configurar OAuth no Google Cloud
- [ ] 6. Testar variáveis de ambiente (Teste 1)
- [ ] 7. Testar login com Google (Teste 2)
---
## 🔍 DIAGNÓSTICO DE PROBLEMAS
### Problema: Variáveis retornam `undefined`
**Solução:**
1. Verifique se adicionou as variáveis no Netlify
2. Verifique se fez o novo deploy
3. Aguarde o deploy finalizar completamente
4. Limpe o cache do navegador (Ctrl+Shift+Delete)
### Problema: "Invalid redirect URL"
**Solução:**
1. Verifique se adicionou as URLs no Supabase (passo 2)
2. Verifique se adicionou as URIs no Google Cloud (passo 3)
3. Aguarde 1-2 minutos para as configurações propagarem
### Problema: Erro 401 após login
**Solução:**
- ✅ Já resolvido! Você executou o SQL corretamente
### Problema: Login funciona mas volta para tela de login
**Solução:**
1. Verifique se as variáveis de ambiente estão configuradas
2. Verifique se o novo deploy foi feito
3. Limpe o cache do navegador
---
## 🎯 RESUMO RÁPIDO
**3 lugares para configurar:**
1. **Netlify** → Variáveis de ambiente + Novo deploy
2. **Supabase** → Redirect URLs
3. **Google Cloud** → Redirect URIs
**Tempo estimado:** 10-15 minutos
---
## 💡 DICA IMPORTANTE
Após configurar tudo, aguarde 2-3 minutos para:
- Deploy do Netlify finalizar
- Configurações do Supabase propagarem
- Configurações do Google Cloud propagarem
Depois teste o login!
---
**Qualquer dúvida, me avise em qual passo você está!**

View File

@@ -1,211 +0,0 @@
# 🔧 CORREÇÃO: Autenticação no Netlify
## 🎯 PROBLEMA IDENTIFICADO
O app funciona localmente (localhost:5173 e :3000) mas falha ao autenticar no Netlify devido a:
1. ❌ Variáveis de ambiente não configuradas no Netlify
2. ❌ URLs de callback OAuth não configuradas no Supabase
3. ❌ Políticas RLS bloqueando acesso após OAuth
---
## ✅ SOLUÇÃO COMPLETA
### 1⃣ CONFIGURAR VARIÁVEIS DE AMBIENTE NO NETLIFY
Acesse: https://app.netlify.com → Seu Site → Site settings → Environment variables
Adicione estas variáveis:
```
VITE_SUPABASE_URL=https://xzudfhifaancyxxfdejx.supabase.co
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inh6dWRmaGlmYWFuY3l4eGZkZWp4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzEzNjE0MTAsImV4cCI6MjA4NjkzNzQxMH0.c5CHWhfXcMrm27LfxEt6OZtttXXvVJOeWu-IbnNLfWY
```
**IMPORTANTE:** Após adicionar, faça um novo deploy!
---
### 2⃣ CONFIGURAR REDIRECT URLs NO SUPABASE
Acesse: https://supabase.com/dashboard/project/xzudfhifaancyxxfdejx/auth/url-configuration
Na seção **Redirect URLs**, adicione:
```
https://SEU-SITE.netlify.app/auth/callback
https://SEU-SITE.netlify.app/*
```
**Exemplo:**
```
https://rdo-tracksteel.netlify.app/auth/callback
https://rdo-tracksteel.netlify.app/*
```
Na seção **Site URL**, configure:
```
https://SEU-SITE.netlify.app
```
---
### 3⃣ CORRIGIR POLÍTICAS RLS PARA OAUTH
O problema principal: quando um usuário faz login via Google OAuth, o Supabase cria o registro em `auth.users`, mas a política RLS impede a criação automática do perfil em `public.usuarios`.
Execute este SQL no Supabase (SQL Editor):
```sql
-- ============================================================================
-- CORREÇÃO RLS PARA OAUTH - PERMITIR CRIAÇÃO AUTOMÁTICA DE PERFIL
-- ============================================================================
-- 1. Permitir INSERT na tabela usuarios para usuários autenticados
DROP POLICY IF EXISTS "Users can create own profile" ON public.usuarios;
CREATE POLICY "Users can create own profile" ON public.usuarios
FOR INSERT
WITH CHECK (auth.uid() = id);
-- 2. Garantir que o trigger handle_new_user funciona
-- Verificar se a função existe
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
-- Inserir novo usuário na tabela public.usuarios
INSERT INTO public.usuarios (
id,
email,
nome,
role,
ativo
) VALUES (
NEW.id,
NEW.email,
COALESCE(NEW.raw_user_meta_data->>'nome', NEW.raw_user_meta_data->>'name', split_part(NEW.email, '@', 1)),
'usuario',
true
)
ON CONFLICT (id) DO UPDATE SET
email = EXCLUDED.email,
nome = COALESCE(EXCLUDED.nome, public.usuarios.nome),
updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- 3. Garantir que o trigger está ativo
DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW
EXECUTE FUNCTION public.handle_new_user();
-- 4. Permitir leitura de organizações para usuários sem organização (signup)
DROP POLICY IF EXISTS "Users can view orgs during signup" ON public.organizacoes;
CREATE POLICY "Users can view orgs during signup" ON public.organizacoes
FOR SELECT
USING (auth.uid() IS NOT NULL);
-- 5. Permitir criação de organização para novos usuários
DROP POLICY IF EXISTS "Users can create org" ON public.organizacoes;
CREATE POLICY "Users can create org" ON public.organizacoes
FOR INSERT
WITH CHECK (auth.uid() IS NOT NULL);
-- ============================================================================
-- FIM DA CORREÇÃO
-- ============================================================================
```
---
### 4⃣ VERIFICAR CONFIGURAÇÃO DO GOOGLE OAUTH
No Google Cloud Console (https://console.cloud.google.com):
1. Acesse: APIs & Services → Credentials
2. Clique no seu OAuth 2.0 Client ID
3. Em **Authorized redirect URIs**, adicione:
```
https://xzudfhifaancyxxfdejx.supabase.co/auth/v1/callback
https://SEU-SITE.netlify.app/auth/callback
```
---
## 🧪 TESTAR A CORREÇÃO
### Teste 1: Variáveis de Ambiente
```bash
# No console do navegador (F12) no site Netlify:
console.log(import.meta.env.VITE_SUPABASE_URL)
```
Deve retornar: `https://xzudfhifaancyxxfdejx.supabase.co`
### Teste 2: Login OAuth
1. Acesse seu site no Netlify
2. Clique em "Login com Google"
3. Autorize o app
4. Deve redirecionar e autenticar com sucesso
### Teste 3: Verificar Perfil Criado
No Supabase SQL Editor:
```sql
SELECT * FROM public.usuarios WHERE email = 'seu-email@gmail.com';
```
---
## 🔍 DIAGNÓSTICO DE PROBLEMAS
### Erro: "Invalid redirect URL"
- ✅ Verifique se adicionou a URL no Supabase (passo 2)
- ✅ Verifique se adicionou no Google Cloud (passo 4)
### Erro: "User not found" ou "Permission denied"
- ✅ Execute o SQL de correção RLS (passo 3)
- ✅ Verifique se o trigger `handle_new_user` está ativo
### Erro: "Environment variables not defined"
- ✅ Configure variáveis no Netlify (passo 1)
- ✅ Faça um novo deploy após adicionar
### Login funciona mas não carrega dados
- ✅ Problema de RLS - execute o SQL do passo 3
- ✅ Verifique se o usuário foi criado em `public.usuarios`
---
## 📋 CHECKLIST FINAL
- [ ] Variáveis de ambiente configuradas no Netlify
- [ ] Novo deploy realizado após configurar variáveis
- [ ] Redirect URLs adicionadas no Supabase
- [ ] Redirect URIs adicionadas no Google Cloud
- [ ] SQL de correção RLS executado no Supabase
- [ ] Teste de login OAuth realizado com sucesso
- [ ] Perfil do usuário criado em `public.usuarios`
---
## 🎯 RESULTADO ESPERADO
Após seguir todos os passos:
✅ Login via Google funciona no Netlify
✅ Perfil do usuário é criado automaticamente
✅ Usuário consegue acessar o app normalmente
✅ RLS permite acesso aos dados da organização
---
**Data:** 24/02/2026
**Status:** Aguardando aplicação das correções

View File

@@ -1,39 +1,26 @@
# Use Node.js 18 Alpine como base
FROM node:18-alpine AS base
# Build stage
FROM node:22-alpine AS build
# Instalar pnpm
RUN npm install -g pnpm
# Build-time environment variables
ARG VITE_SUPABASE_URL
ARG VITE_SUPABASE_ANON_KEY
# Update packages for security
RUN apk update && apk upgrade --no-cache
# Definir diretório de trabalho
WORKDIR /app
# Copiar arquivos de dependências
COPY package.json pnpm-lock.yaml ./
# Instalar dependências
RUN pnpm install --frozen-lockfile
# Copiar código fonte
COPY package*.json ./
RUN npm install --legacy-peer-deps --no-audit --no-fund
COPY . .
RUN npm run build
# Build da aplicação
RUN pnpm run build
# Production stage
FROM nginx:alpine
# Estágio de produção
FROM node:18-alpine AS production
# Update packages for security
RUN apk update && apk upgrade --no-cache
# Instalar pnpm e serve
RUN npm install -g pnpm serve
# Definir diretório de trabalho
WORKDIR /app
# Copiar arquivos buildados
COPY --from=base /app/dist ./dist
COPY --from=base /app/package.json ./
# Expor porta
EXPOSE $PORT
# Comando para iniciar o servidor
CMD ["sh", "-c", "serve -s dist -l ${PORT:-3000}"]
COPY --from=build /app/dist /usr/share/nginx/html
# Nginx default listen 80
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -0,0 +1,104 @@
# 🧪 TESTE DE VARIÁVEIS NO NAVEGADOR
## ❌ ERRO NO CONSOLE
O comando `import.meta.env` NÃO funciona no console do navegador!
Ele só funciona dentro do código da aplicação durante o build.
---
## ✅ COMO TESTAR CORRETAMENTE
### Opção 1: Verificar no código fonte
1. Abra: https://rdo.tracksteel.com.br
2. Pressione **F12** (DevTools)
3. Vá na aba **Sources** ou **Debugger**
4. Procure por arquivos `.js` no painel esquerdo
5. Abra qualquer arquivo e procure por `xzudfhifaancyxxfdejx`
6. Se encontrar = ✅ Variáveis configuradas!
7. Se NÃO encontrar = ❌ Variáveis não foram injetadas no build
---
### Opção 2: Verificar no Network
1. Abra: https://rdo.tracksteel.com.br
2. Pressione **F12** (DevTools)
3. Vá na aba **Network**
4. Clique em "Login com Google"
5. Procure por requisições para `xzudfhifaancyxxfdejx.supabase.co`
6. Se aparecer = ✅ Variáveis configuradas!
7. Se NÃO aparecer = ❌ Variáveis não foram injetadas
---
### Opção 3: Adicionar console.log temporário
Adicione este código no arquivo `src/lib/supabase.ts` (linha 6):
```typescript
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
// ADICIONAR ESTAS LINHAS TEMPORÁRIAS:
console.log('🔍 SUPABASE URL:', supabaseUrl)
console.log('🔍 SUPABASE KEY:', supabaseAnonKey?.substring(0, 30) + '...')
```
Depois faça commit, push e aguarde o deploy no Netlify.
Quando abrir o site, vai aparecer no console!
---
## 🚨 PROBLEMA IDENTIFICADO
O erro **401 Unauthorized** com `grant_type=pkce` indica que:
1. ✅ As variáveis de ambiente ESTÃO configuradas (senão daria outro erro)
2. ❌ O flowType estava configurado como `pkce` que não funciona bem em produção
3. ✅ JÁ CORRIGI mudando para `implicit`
---
## 📋 PRÓXIMOS PASSOS
1.**Código corrigido** (mudei flowType para implicit)
2.**Push feito** para o GitHub
3.**Aguardar deploy** no Netlify (2-3 minutos)
4. 🧪 **Testar login** novamente
---
## ⏰ AGUARDE O DEPLOY
O Netlify precisa fazer o build novamente com o código corrigido.
**Como verificar se o deploy terminou:**
1. Acesse: https://app.netlify.com
2. Clique no seu site
3. Vá em **Deploys**
4. Aguarde o deploy mais recente ficar **"Published"** (verde)
5. Depois teste o login!
---
## 🎯 TESTE APÓS O DEPLOY
1. Abra: https://rdo.tracksteel.com.br
2. Limpe o cache (Ctrl+Shift+Delete → Limpar tudo)
3. Recarregue a página (Ctrl+F5)
4. Clique em "Login com Google"
5. Deve funcionar agora!
---
## 🔍 SE AINDA DER ERRO
Me envie print do console (F12) mostrando:
- Aba **Console** (mensagens de erro)
- Aba **Network** (requisições falhando)

8
TS_RDO.code-workspace Normal file
View File

@@ -0,0 +1,8 @@
{
"folders": [
{
"path": "TS_RDO"
}
],
"settings": {}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
import{j as e}from"./query-vendor-BLVqILA6.js";import{d as w,r as t}from"./router-vendor-D4by-_6Z.js";import{s as i}from"./index-doec96Hx.js";import{a0 as j,c as b,a2 as N}from"./ui-vendor-CyRvbSfR.js";import"./react-vendor-CqRd3GwO.js";import"./supabase-vendor-CnnNSQLo.js";import"./state-vendor-DHadhBU5.js";const _=()=>{const o=w(),[s,c]=t.useState("loading"),[g,n]=t.useState("Processando autenticação..."),l=t.useCallback(async()=>{try{const a=new URLSearchParams(window.location.search),d=a.get("error"),x=a.get("error_description");if(d)throw new Error(x||d);const{data:{session:r},error:m}=await i.auth.getSession();if(m)throw m;if(!r)throw new Error("Nenhuma sessão encontrada");const{data:u,error:h}=await i.from("usuarios").select("id, organizacao_id").eq("id",r.user.id).single();if(!u&&h?.code==="PGRST116"){const{error:p}=await i.from("usuarios").insert({id:r.user.id,email:r.user.email,nome:r.user.user_metadata.full_name||r.user.email?.split("@")[0]})}c("success"),n("Login realizado com sucesso!");const f=u?.organizacao_id;setTimeout(()=>{o(f?"/":"/selecionar-organizacao")},1e3)}catch(a){c("error"),n(a instanceof Error?a.message:"Erro ao processar autenticação"),setTimeout(()=>{o("/login")},3e3)}},[o]);return t.useEffect(()=>{l()},[l]),e.jsx("div",{className:"min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4",children:e.jsx("div",{className:"bg-white rounded-2xl shadow-xl p-8 max-w-md w-full",children:e.jsxs("div",{className:"flex flex-col items-center space-y-4",children:[s==="loading"&&e.jsx(j,{className:"w-16 h-16 text-blue-500 animate-spin"}),s==="success"&&e.jsx(b,{className:"w-16 h-16 text-green-500"}),s==="error"&&e.jsx(N,{className:"w-16 h-16 text-red-500"}),e.jsxs("div",{className:"text-center",children:[e.jsxs("h2",{className:"text-2xl font-bold text-gray-900 mb-2",children:[s==="loading"&&"Processando...",s==="success"&&"Sucesso!",s==="error"&&"Erro"]}),e.jsx("p",{className:"text-gray-600",children:g})]}),s==="loading"&&e.jsx("div",{className:"w-full bg-gray-200 rounded-full h-2 overflow-hidden",children:e.jsx("div",{className:"bg-blue-500 h-2 rounded-full animate-pulse w-2/3"})}),s!=="loading"&&e.jsx("p",{className:"text-sm text-gray-500",children:s==="success"?"Redirecionando para o sistema...":"Redirecionando para o login..."})]})})})};export{_ as AuthCallback};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
import{j as M}from"./query-vendor-BLVqILA6.js";import{r as c}from"./router-vendor-D4by-_6Z.js";const E=()=>{const l=c.useRef(null),h=c.useRef(),n=c.useRef([]);return c.useEffect(()=>{const e=l.current;if(!e)return;const r=e.getContext("2d");if(!r)return;const f=()=>{e.width=window.innerWidth,e.height=window.innerHeight},u=()=>{const t=Math.floor(e.width*e.height/15e3);n.current=[];for(let a=0;a<t;a++)n.current.push({x:Math.random()*e.width,y:Math.random()*e.height,vx:(Math.random()-.5)*.5,vy:(Math.random()-.5)*.5})},g=t=>{r.beginPath(),r.arc(t.x,t.y,2,0,Math.PI*2),r.fillStyle="rgba(147, 197, 253, 0.8)",r.fill()},y=(t,a,i,o)=>{const s=1-i/o;r.beginPath(),r.moveTo(t.x,t.y),r.lineTo(a.x,a.y),r.strokeStyle=`rgba(147, 197, 253, ${s*.3})`,r.lineWidth=1,r.stroke()},v=()=>{n.current.forEach(t=>{t.x+=t.vx,t.y+=t.vy,(t.x<0||t.x>e.width)&&(t.vx*=-1),(t.y<0||t.y>e.height)&&(t.vy*=-1),t.x=Math.max(0,Math.min(e.width,t.x)),t.y=Math.max(0,Math.min(e.height,t.y))})},x=()=>{r.clearRect(0,0,e.width,e.height);const t=r.createLinearGradient(0,0,e.width,e.height);t.addColorStop(0,"#0f172a"),t.addColorStop(.5,"#1e1b4b"),t.addColorStop(1,"#581c87"),r.fillStyle=t,r.fillRect(0,0,e.width,e.height),v();const a=120;for(let i=0;i<n.current.length;i++)for(let o=i+1;o<n.current.length;o++){const s=n.current[i],d=n.current[o],w=Math.sqrt(Math.pow(s.x-d.x,2)+Math.pow(s.y-d.y,2));w<a&&y(s,d,w,a)}n.current.forEach(g),h.current=requestAnimationFrame(x)};f(),u(),x();const m=()=>{f(),u()};return window.addEventListener("resize",m),()=>{window.removeEventListener("resize",m),h.current&&cancelAnimationFrame(h.current)}},[]),M.jsx("canvas",{ref:l,className:"fixed inset-0 w-full h-full -z-10",style:{background:"linear-gradient(135deg, #0f172a 0%, #1e1b4b 50%, #581c87 100%)"}})};export{E as N};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
import{j as a}from"./query-vendor-BLVqILA6.js";import{r as o}from"./router-vendor-D4by-_6Z.js";import{m as s,I as l,am as i}from"./ui-vendor-CyRvbSfR.js";function n(){const[t,e]=o.useState(()=>{const r=localStorage.getItem("theme");return r||(window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light")});return o.useEffect(()=>{document.documentElement.classList.remove("light","dark"),document.documentElement.classList.add(t),localStorage.setItem("theme",t)},[t]),{theme:t,toggleTheme:()=>{e(r=>r==="light"?"dark":"light")},isDark:t==="dark"}}function h(){const{toggleTheme:t,isDark:e}=n();return a.jsx(s.button,{onClick:t,className:"relative p-2 rounded-xl bg-white/70 dark:bg-gray-800/70 backdrop-blur-md border border-gray-200/50 dark:border-gray-700/50 shadow-lg hover:shadow-xl transition-all duration-300 group",whileHover:{scale:1.05},whileTap:{scale:.95},title:e?"Alternar para modo claro":"Alternar para modo escuro",children:a.jsxs("div",{className:"relative w-6 h-6",children:[a.jsx(s.div,{initial:!1,animate:{scale:e?0:1,rotate:e?180:0,opacity:e?0:1},transition:{duration:.3,ease:"easeInOut"},className:"absolute inset-0 flex items-center justify-center",children:a.jsx(l,{className:"w-5 h-5 text-yellow-500 group-hover:text-yellow-600"})}),a.jsx(s.div,{initial:!1,animate:{scale:e?1:0,rotate:e?0:-180,opacity:e?1:0},transition:{duration:.3,ease:"easeInOut"},className:"absolute inset-0 flex items-center justify-center",children:a.jsx(i,{className:"w-5 h-5 text-blue-400 group-hover:text-blue-300"})})]})})}export{h as T};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +0,0 @@
import{r as d}from"./router-vendor-D4by-_6Z.js";import{s as n}from"./index-doec96Hx.js";const w=()=>{const[g,s]=d.useState(!1),[m,t]=d.useState(null);return{loading:g,error:m,validarConvite:async i=>{try{s(!0),t(null);const e=i.toUpperCase().trim(),{data:r,error:a}=await n.from("convites").select(`
id,
codigo,
organizacao_id,
role,
max_usos,
usos_atuais,
ativo,
expira_em,
email_convidado,
organizacoes:organizacao_id (nome)
`).eq("codigo",e).eq("ativo",!0).single();if(a||!r)return{success:!1,error:"Código de convite inválido ou expirado."};const o=r;return o.expira_em&&new Date(o.expira_em)<new Date?{success:!1,error:"Este código de convite expirou."}:o.max_usos>0&&o.usos_atuais>=o.max_usos?{success:!1,error:"Este código de convite já atingiu o limite de usos."}:{success:!0,organizacao_id:o.organizacao_id,organizacao_nome:o.organizacoes?.nome||"Organização",role:o.role}}catch(e){const r=e instanceof Error?e.message:"Erro ao validar convite";return t(r),{success:!1,error:r}}finally{s(!1)}},usarConvite:async(i,e)=>{try{s(!0),t(null);const{data:r,error:a}=await n.rpc("usar_convite",{p_codigo:i,p_usuario_id:e});if(a)throw a;const o=r;return o.success||t(o.error||"Erro ao usar convite"),o}catch(r){const a=r instanceof Error?r.message:"Erro ao usar convite";return t(a),{success:!1,error:a}}finally{s(!1)}},gerarConvite:async(i,e={})=>{try{s(!0),t(null);const{data:r,error:a}=await n.rpc("gerar_codigo_convite");if(a)throw a;const o=r;let u=null;if(e.expiraEmDias){const c=new Date;c.setDate(c.getDate()+e.expiraEmDias),u=c.toISOString()}const{data:{user:f}}=await n.auth.getUser(),{error:l}=await n.from("convites").insert({organizacao_id:i,codigo:o,criado_por:f?.id,email_convidado:e.emailConvidado||null,role:e.role||"usuario",max_usos:e.maxUsos??1,expira_em:u});if(l)throw l;return{success:!0,codigo:o}}catch(r){const a=r instanceof Error?r.message:"Erro ao gerar convite";return t(a),{success:!1,error:a}}finally{s(!1)}},listarConvites:async i=>{try{s(!0);const{data:e,error:r}=await n.from("convites").select("*").eq("organizacao_id",i).order("created_at",{ascending:!1});if(r)throw r;return e||[]}catch{return[]}finally{s(!1)}}}};export{w as u};

View File

@@ -1 +0,0 @@
import{d,p as U,s as o}from"./index-doec96Hx.js";import{c as g}from"./state-vendor-DHadhBU5.js";import"./query-vendor-BLVqILA6.js";import"./router-vendor-D4by-_6Z.js";import"./react-vendor-CqRd3GwO.js";import"./ui-vendor-CyRvbSfR.js";import"./supabase-vendor-CnnNSQLo.js";const i={currentUser:null,users:[],loading:!1,error:null},m=g()(d(U((r,u)=>({...i,setCurrentUser:e=>r({currentUser:e},!1,"setCurrentUser"),setUsers:e=>r({users:e},!1,"setUsers"),setLoading:e=>r({loading:e},!1,"setLoading"),setError:e=>r({error:e},!1,"setError"),clearError:()=>r({error:null},!1,"clearError"),reset:()=>r(i,!1,"reset"),fetchCurrentUser:async e=>{try{r({loading:!0,error:null},!1,"fetchCurrentUser:start");const{data:s,error:a}=await o.from("usuarios").select("*").eq("id",e).single();if(a)throw a;r({currentUser:s,loading:!1},!1,"fetchCurrentUser:success")}catch(s){r({error:s.message||"Erro ao buscar usuário",loading:!1},!1,"fetchCurrentUser:error")}},fetchUsers:async()=>{try{r({loading:!0,error:null},!1,"fetchUsers:start");const{data:e,error:s}=await o.from("usuarios").select("*").order("nome");if(s)throw s;r({users:e||[],loading:!1},!1,"fetchUsers:success")}catch(e){r({error:e.message||"Erro ao buscar usuários",loading:!1},!1,"fetchUsers:error")}},updateUser:async(e,s)=>{try{r({loading:!0,error:null},!1,"updateUser:start");const{data:a,error:t}=await o.from("usuarios").update({...s,updated_at:new Date().toISOString()}).eq("id",e).select().single();if(t)throw t;const{users:n,currentUser:l}=u(),f=n.map(c=>c.id===e?a:c);return r({users:f,currentUser:l?.id===e?a:l,loading:!1},!1,"updateUser:success"),!0}catch(a){return r({error:a.message||"Erro ao atualizar usuário",loading:!1},!1,"updateUser:error"),!1}},createUser:async e=>{try{r({loading:!0,error:null},!1,"createUser:start");const{data:s,error:a}=await o.from("usuarios").insert(e).select().single();if(a)throw a;const{users:t}=u();return r({users:[...t,s],loading:!1},!1,"createUser:success"),!0}catch(s){return r({error:s.message||"Erro ao criar usuário",loading:!1},!1,"createUser:error"),!1}},deleteUser:async e=>{try{r({loading:!0,error:null},!1,"deleteUser:start");const{error:s}=await o.from("usuarios").delete().eq("id",e);if(s)throw s;const{users:a,currentUser:t}=u(),n=a.filter(l=>l.id!==e);return r({users:n,currentUser:t?.id===e?null:t,loading:!1},!1,"deleteUser:success"),!0}catch(s){return r({error:s.message||"Erro ao deletar usuário",loading:!1},!1,"deleteUser:error"),!1}}}),{name:"user-store",partialize:r=>({currentUser:r.currentUser,users:r.users})}))),b=()=>m(r=>r.currentUser);export{b as useCurrentUser,m as useUserStore};

View File

@@ -1 +0,0 @@
import{a as p,b as y,c as v}from"./form-vendor-vQotxSmE.js";function g(r,s,o){function e(t,c){var u;Object.defineProperty(t,"_zod",{value:t._zod??{},enumerable:!1}),(u=t._zod).traits??(u.traits=new Set),t._zod.traits.add(r),s(t,c);for(const f in n.prototype)f in t||Object.defineProperty(t,f,{value:n.prototype[f].bind(t)});t._zod.constr=n,t._zod.def=c}const a=o?.Parent??Object;class i extends a{}Object.defineProperty(i,"name",{value:r});function n(t){var c;const u=o?.Parent?new i:this;e(u,t),(c=u._zod).deferred??(c.deferred=[]);for(const f of u._zod.deferred)f();return u}return Object.defineProperty(n,"init",{value:e}),Object.defineProperty(n,Symbol.hasInstance,{value:t=>o?.Parent&&t instanceof o.Parent?!0:t?._zod?.traits?.has(r)}),Object.defineProperty(n,"name",{value:r}),n}class j extends Error{constructor(){super("Encountered Promise during synchronous parse. Use .parseAsync() instead.")}}const z={};function m(r){return z}function w(r,s){return typeof s=="bigint"?s.toString():s}const b=Error.captureStackTrace?Error.captureStackTrace:(...r)=>{};function d(r){return typeof r=="string"?r:r?.message}function E(r,s,o){const e={...r,path:r.path??[]};if(!r.message){const a=d(r.inst?._zod.def?.error?.(r))??d(s?.error?.(r))??d(o.customError?.(r))??d(o.localeError?.(r))??"Invalid input";e.message=a}return delete e.inst,delete e.continue,s?.reportInput||delete e.input,e}const P=(r,s)=>{r.name="$ZodError",Object.defineProperty(r,"_zod",{value:r._zod,enumerable:!1}),Object.defineProperty(r,"issues",{value:s,enumerable:!1}),Object.defineProperty(r,"message",{get(){return JSON.stringify(s,w,2)},enumerable:!0}),Object.defineProperty(r,"toString",{value:()=>r.message,enumerable:!1})},O=g("$ZodError",P),_=g("$ZodError",P,{Parent:Error}),S=r=>(s,o,e,a)=>{const i=e?Object.assign(e,{async:!1}):{async:!1},n=s._zod.run({value:o,issues:[]},i);if(n instanceof Promise)throw new j;if(n.issues.length){const t=new(a?.Err??r)(n.issues.map(c=>E(c,i,m())));throw b(t,a?.callee),t}return n.value},A=S(_),Z=r=>async(s,o,e,a)=>{const i=e?Object.assign(e,{async:!0}):{async:!0};let n=s._zod.run({value:o,issues:[]},i);if(n instanceof Promise&&(n=await n),n.issues.length){const t=new(a?.Err??r)(n.issues.map(c=>E(c,i,m())));throw b(t,a?.callee),t}return n.value},N=Z(_);function h(r,s){try{var o=r()}catch(e){return s(e)}return o&&o.then?o.then(void 0,s):o}function I(r,s){for(var o={};r.length;){var e=r[0],a=e.code,i=e.message,n=e.path.join(".");if(!o[n])if("unionErrors"in e){var t=e.unionErrors[0].errors[0];o[n]={message:t.message,type:t.code}}else o[n]={message:i,type:a};if("unionErrors"in e&&e.unionErrors.forEach(function(f){return f.errors.forEach(function(l){return r.push(l)})}),s){var c=o[n].types,u=c&&c[e.code];o[n]=v(n,s,o,a,u?[].concat(u,e.message):e.message)}r.shift()}return o}function U(r,s){for(var o={};r.length;){var e=r[0],a=e.code,i=e.message,n=e.path.join(".");if(!o[n])if(e.code==="invalid_union"&&e.errors.length>0){var t=e.errors[0][0];o[n]={message:t.message,type:t.code}}else o[n]={message:i,type:a};if(e.code==="invalid_union"&&e.errors.forEach(function(f){return f.forEach(function(l){return r.push(l)})}),s){var c=o[n].types,u=c&&c[e.code];o[n]=v(n,s,o,a,u?[].concat(u,e.message):e.message)}r.shift()}return o}function k(r,s,o){if(o===void 0&&(o={}),(function(e){return"_def"in e&&typeof e._def=="object"&&"typeName"in e._def})(r))return function(e,a,i){try{return Promise.resolve(h(function(){return Promise.resolve(r[o.mode==="sync"?"parse":"parseAsync"](e,s)).then(function(n){return i.shouldUseNativeValidation&&p({},i),{errors:{},values:o.raw?Object.assign({},e):n}})},function(n){if((function(t){return Array.isArray(t?.issues)})(n))return{values:{},errors:y(I(n.errors,!i.shouldUseNativeValidation&&i.criteriaMode==="all"),i)};throw n}))}catch(n){return Promise.reject(n)}};if((function(e){return"_zod"in e&&typeof e._zod=="object"})(r))return function(e,a,i){try{return Promise.resolve(h(function(){return Promise.resolve((o.mode==="sync"?A:N)(r,e,s)).then(function(n){return i.shouldUseNativeValidation&&p({},i),{errors:{},values:o.raw?Object.assign({},e):n}})},function(n){if((function(t){return t instanceof O})(n))return{values:{},errors:y(U(n.issues,!i.shouldUseNativeValidation&&i.criteriaMode==="all"),i)};throw n}))}catch(n){return Promise.reject(n)}};throw new Error("Invalid input: not a Zod schema")}export{k as a};

File diff suppressed because one or more lines are too long

18
dist/index.html vendored
View File

@@ -8,14 +8,16 @@
<meta name="theme-color" content="#2563eb" />
<link rel="manifest" href="/manifest.json" />
<title>RDO Mobile - Relatório Diário de Obra</title>
<script type="module" crossorigin src="/assets/js/index-doec96Hx.js"></script>
<link rel="modulepreload" crossorigin href="/assets/js/react-vendor-CqRd3GwO.js">
<link rel="modulepreload" crossorigin href="/assets/js/router-vendor-D4by-_6Z.js">
<link rel="modulepreload" crossorigin href="/assets/js/query-vendor-BLVqILA6.js">
<link rel="modulepreload" crossorigin href="/assets/js/ui-vendor-CyRvbSfR.js">
<link rel="modulepreload" crossorigin href="/assets/js/supabase-vendor-CnnNSQLo.js">
<link rel="modulepreload" crossorigin href="/assets/js/state-vendor-DHadhBU5.js">
<link rel="stylesheet" crossorigin href="/assets/css/index-DXaaJsOA.css">
<script type="module" crossorigin src="/assets/index-BFMh0Owy.js"></script>
<link rel="modulepreload" crossorigin href="/assets/react-DtrESx-C.js">
<link rel="modulepreload" crossorigin href="/assets/preload-helper-DSXbuxSR.js">
<link rel="modulepreload" crossorigin href="/assets/supabase-L170XLdN.js">
<link rel="modulepreload" crossorigin href="/assets/createLucideIcon-BLxppFDo.js">
<link rel="modulepreload" crossorigin href="/assets/jsx-runtime-DNOlzffn.js">
<link rel="modulepreload" crossorigin href="/assets/react-eH9hKoTL.js">
<link rel="modulepreload" crossorigin href="/assets/useUserStore-Btl1TeSc.js">
<link rel="modulepreload" crossorigin href="/assets/useAuth-Bw9Uh8UY.js">
<link rel="stylesheet" crossorigin href="/assets/index-B3UgNpvV.css">
</head>
<body>
<div id="root"></div>

50
fix_rls_disable.sql Normal file
View File

@@ -0,0 +1,50 @@
-- ============================================================================
-- REMOVER TODAS AS POLÍTICAS RLS DO SCHEMA 'rdo'
-- Execute este script no SQL Editor do Supabase
-- ============================================================================
-- Desabilitar RLS em todas as tabelas
ALTER TABLE rdo.usuarios DISABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.organizacoes DISABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.obras DISABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.rdos DISABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.rdo_atividades DISABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.rdo_mao_obra DISABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.rdo_equipamentos DISABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.rdo_ocorrencias DISABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.rdo_anexos DISABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.rdo_inspecoes_solda DISABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.rdo_verificacoes_torque DISABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.tarefas DISABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.inventario_equipamentos DISABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.task_logs DISABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.tipos_atividade DISABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.condicoes_climaticas DISABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.tipos_ocorrencia DISABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.funcoes_cargos DISABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.equipamentos DISABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.materiais DISABLE ROW LEVEL SECURITY;
-- Dropar políticas existentes
DROP POLICY IF EXISTS "auth_all_rdo_usuarios" ON rdo.usuarios;
DROP POLICY IF EXISTS "auth_all_rdo_orgs" ON rdo.organizacoes;
DROP POLICY IF EXISTS "auth_all_rdo_obras" ON rdo.obras;
DROP POLICY IF EXISTS "auth_all_rdo_rdos" ON rdo.rdos;
DROP POLICY IF EXISTS "auth_all_rdo_ativ" ON rdo.rdo_atividades;
DROP POLICY IF EXISTS "auth_all_rdo_mao" ON rdo.rdo_mao_obra;
DROP POLICY IF EXISTS "auth_all_rdo_equip" ON rdo.rdo_equipamentos;
DROP POLICY IF EXISTS "auth_all_rdo_ocor" ON rdo.rdo_ocorrencias;
DROP POLICY IF EXISTS "auth_all_rdo_anex" ON rdo.rdo_anexos;
DROP POLICY IF EXISTS "auth_all_rdo_insp" ON rdo.rdo_inspecoes_solda;
DROP POLICY IF EXISTS "auth_all_rdo_torq" ON rdo.rdo_verificacoes_torque;
DROP POLICY IF EXISTS "auth_all_rdo_tarefas" ON rdo.tarefas;
DROP POLICY IF EXISTS "auth_all_rdo_inv_equip" ON rdo.inventario_equipamentos;
DROP POLICY IF EXISTS "auth_all_rdo_task_logs" ON rdo.task_logs;
DROP POLICY IF EXISTS "auth_all_rdo_tipos_ativ" ON rdo.tipos_atividade;
DROP POLICY IF EXISTS "auth_all_rdo_cond_clim" ON rdo.condicoes_climaticas;
DROP POLICY IF EXISTS "auth_all_rdo_tipos_ocor" ON rdo.tipos_ocorrencia;
DROP POLICY IF EXISTS "auth_all_rdo_func" ON rdo.funcoes_cargos;
DROP POLICY IF EXISTS "auth_all_rdo_equip" ON rdo.equipamentos;
DROP POLICY IF EXISTS "auth_all_rdo_materiais" ON rdo.materiais;
SELECT 'RLS desabilitado em todas as tabelas do schema rdo!' AS resultado;

275
migrate_rdo_schema.sql Normal file
View File

@@ -0,0 +1,275 @@
-- ============================================================================
-- MIGRAÇÃO COMPLETA DO SCHEMA 'rdo' PARA SUPABASE
-- Executar este script no SQL Editor do Supabase
-- ============================================================================
-- 1. CRIAR ESQUEMA
-- ============================================================================
CREATE SCHEMA IF NOT EXISTS rdo;
-- 2. GARANTIR EXTENSÕES
-- ============================================================================
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- 3. CRIAR TABELAS NO ESQUEMA 'rdo'
-- ============================================================================
-- Tabela: Organizacoes
CREATE TABLE IF NOT EXISTS rdo.organizacoes (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
nome TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
razao_social TEXT,
cnpj TEXT,
status TEXT DEFAULT 'ativa' CHECK (status IN ('ativa', 'inativa', 'suspensa')),
plano TEXT DEFAULT 'trial' CHECK (plano IN ('trial', 'basico', 'profissional', 'enterprise')),
max_usuarios INTEGER DEFAULT 5,
max_obras INTEGER DEFAULT 10,
max_rdos_mes INTEGER DEFAULT 100,
max_storage_mb INTEGER DEFAULT 1024,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Tabela: Usuarios
CREATE TABLE IF NOT EXISTS rdo.usuarios (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
organizacao_id UUID REFERENCES rdo.organizacoes(id) ON DELETE SET NULL,
email TEXT NOT NULL,
nome TEXT NOT NULL,
telefone TEXT,
cargo TEXT,
role TEXT DEFAULT 'usuario' CHECK (role IN ('dev', 'admin', 'engenheiro', 'mestre_obra', 'usuario')),
ativo BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Tabela: Obras
CREATE TABLE IF NOT EXISTS rdo.obras (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
organizacao_id UUID NOT NULL REFERENCES rdo.organizacoes(id) ON DELETE CASCADE,
nome TEXT NOT NULL,
descricao TEXT,
endereco TEXT,
cep TEXT,
cidade TEXT,
estado TEXT,
responsavel_id UUID REFERENCES rdo.usuarios(id) ON DELETE SET NULL,
data_inicio DATE,
data_prevista_fim DATE,
data_conclusao DATE,
progresso_geral NUMERIC(5,2) DEFAULT 0,
status TEXT DEFAULT 'ativa' CHECK (status IN ('ativa', 'pausada', 'concluida', 'cancelada')),
configuracoes JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Tabela: RDOs
CREATE TABLE IF NOT EXISTS rdo.rdos (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
organizacao_id UUID REFERENCES rdo.organizacoes(id) ON DELETE CASCADE,
obra_id UUID NOT NULL REFERENCES rdo.obras(id) ON DELETE CASCADE,
criado_por UUID NOT NULL REFERENCES rdo.usuarios(id),
data_relatorio DATE NOT NULL,
condicoes_climaticas TEXT NOT NULL,
observacoes_gerais TEXT,
status TEXT DEFAULT 'rascunho' CHECK (status IN ('rascunho', 'enviado', 'aprovado', 'rejeitado')),
aprovado_por UUID REFERENCES rdo.usuarios(id),
aprovado_em TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Tabela: RDO Atividades
CREATE TABLE IF NOT EXISTS rdo.rdo_atividades (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
rdo_id UUID NOT NULL REFERENCES rdo.rdos(id) ON DELETE CASCADE,
tipo_atividade TEXT NOT NULL,
descricao TEXT NOT NULL,
localizacao TEXT,
percentual_concluido NUMERIC(5,2) DEFAULT 0,
ordem INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Tabela: RDO Mão de Obra
CREATE TABLE IF NOT EXISTS rdo.rdo_mao_obra (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
rdo_id UUID NOT NULL REFERENCES rdo.rdos(id) ON DELETE CASCADE,
funcao TEXT NOT NULL,
quantidade INTEGER DEFAULT 0,
horas_trabalhadas NUMERIC(5,2) DEFAULT 0,
observacoes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Tabela: RDO Equipamentos
CREATE TABLE IF NOT EXISTS rdo.rdo_equipamentos (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
rdo_id UUID NOT NULL REFERENCES rdo.rdos(id) ON DELETE CASCADE,
nome_equipamento TEXT NOT NULL,
tipo TEXT,
horas_utilizadas NUMERIC(5,2) DEFAULT 0,
combustivel_gasto NUMERIC(10,2) DEFAULT 0,
observacoes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Tabela: RDO Ocorrencias
CREATE TABLE IF NOT EXISTS rdo.rdo_ocorrencias (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
rdo_id UUID NOT NULL REFERENCES rdo.rdos(id) ON DELETE CASCADE,
tipo_ocorrencia TEXT NOT NULL,
descricao TEXT NOT NULL,
gravidade TEXT CHECK (gravidade IN ('baixa', 'media', 'alta', 'critica')),
acao_tomada TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Tabela: RDO Anexos
CREATE TABLE IF NOT EXISTS rdo.rdo_anexos (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
rdo_id UUID NOT NULL REFERENCES rdo.rdos(id) ON DELETE CASCADE,
nome_arquivo TEXT NOT NULL,
tipo_arquivo TEXT,
url_storage TEXT NOT NULL,
tamanho_bytes BIGINT,
descricao TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Tabela: Tipos de Atividade (configuração)
CREATE TABLE IF NOT EXISTS rdo.tipos_atividade (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
nome TEXT NOT NULL,
descricao TEXT,
ativo BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Tabela: Condições Climáticas (configuração)
CREATE TABLE IF NOT EXISTS rdo.condicoes_climaticas (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
nome TEXT NOT NULL,
descricao TEXT,
ativo BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 4. CRIAR ÍNDICES PARA OTIMIZAÇÃO
-- ============================================================================
CREATE INDEX IF NOT EXISTS idx_usuarios_org ON rdo.usuarios(organizacao_id);
CREATE INDEX IF NOT EXISTS idx_obras_org ON rdo.obras(organizacao_id);
CREATE INDEX IF NOT EXISTS idx_obras_status ON rdo.obras(status);
CREATE INDEX IF NOT EXISTS idx_rdos_obra ON rdo.rdos(obra_id);
CREATE INDEX IF NOT EXISTS idx_rdos_data ON rdo.rdos(data_relatorio);
CREATE INDEX IF NOT EXISTS idx_rdos_status ON rdo.rdos(status);
CREATE INDEX IF NOT EXISTS idx_rdo_atividades_rdo ON rdo.rdo_atividades(rdo_id);
CREATE INDEX IF NOT EXISTS idx_rdo_mao_obra_rdo ON rdo.rdo_mao_obra(rdo_id);
CREATE INDEX IF NOT EXISTS idx_rdo_equipamentos_rdo ON rdo.rdo_equipamentos(rdo_id);
CREATE INDEX IF NOT EXISTS idx_rdo_ocorrencias_rdo ON rdo.rdo_ocorrencias(rdo_id);
-- 5. HABILITAR RLS
-- ============================================================================
ALTER TABLE rdo.organizacoes ENABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.usuarios ENABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.obras ENABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.rdos ENABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.rdo_atividades ENABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.rdo_mao_obra ENABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.rdo_equipamentos ENABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.rdo_ocorrencias ENABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.rdo_anexos ENABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.tipos_atividade ENABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.condicoes_climaticas ENABLE ROW LEVEL SECURITY;
-- 6. CRIAR POLÍTICAS RLS
-- ============================================================================
CREATE POLICY "auth_all_rdo_usuarios" ON rdo.usuarios FOR ALL USING (auth.uid() IS NOT NULL) WITH CHECK (auth.uid() IS NOT NULL);
CREATE POLICY "auth_all_rdo_orgs" ON rdo.organizacoes FOR ALL USING (auth.uid() IS NOT NULL) WITH CHECK (auth.uid() IS NOT NULL);
CREATE POLICY "auth_all_rdo_obras" ON rdo.obras FOR ALL USING (auth.uid() IS NOT NULL) WITH CHECK (auth.uid() IS NOT NULL);
CREATE POLICY "auth_all_rdo_rdos" ON rdo.rdos FOR ALL USING (auth.uid() IS NOT NULL) WITH CHECK (auth.uid() IS NOT NULL);
CREATE POLICY "auth_all_rdo_ativ" ON rdo.rdo_atividades FOR ALL USING (auth.uid() IS NOT NULL) WITH CHECK (auth.uid() IS NOT NULL);
CREATE POLICY "auth_all_rdo_mao" ON rdo.rdo_mao_obra FOR ALL USING (auth.uid() IS NOT NULL) WITH CHECK (auth.uid() IS NOT NULL);
CREATE POLICY "auth_all_rdo_equip" ON rdo.rdo_equipamentos FOR ALL USING (auth.uid() IS NOT NULL) WITH CHECK (auth.uid() IS NOT NULL);
CREATE POLICY "auth_all_rdo_ocor" ON rdo.rdo_ocorrencias FOR ALL USING (auth.uid() IS NOT NULL) WITH CHECK (auth.uid() IS NOT NULL);
CREATE POLICY "auth_all_rdo_anex" ON rdo.rdo_anexos FOR ALL USING (auth.uid() IS NOT NULL) WITH CHECK (auth.uid() IS NOT NULL);
CREATE POLICY "auth_all_rdo_tipos_ativ" ON rdo.tipos_atividade FOR ALL USING (auth.uid() IS NOT NULL) WITH CHECK (auth.uid() IS NOT NULL);
CREATE POLICY "auth_all_rdo_cond_clim" ON rdo.condicoes_climaticas FOR ALL USING (auth.uid() IS NOT NULL) WITH CHECK (auth.uid() IS NOT NULL);
-- 7. PERMISSÕES
-- ============================================================================
GRANT USAGE ON SCHEMA rdo TO authenticated, anon;
GRANT ALL ON ALL TABLES IN SCHEMA rdo TO authenticated;
GRANT ALL ON ALL SEQUENCES IN SCHEMA rdo TO authenticated;
GRANT ALL ON ALL FUNCTIONS IN SCHEMA rdo TO authenticated;
-- 8. CRIAR TRIGGER PARA NOVO USUÁRIO
-- ============================================================================
CREATE OR REPLACE FUNCTION rdo.handle_new_user()
RETURNS TRIGGER
SECURITY DEFINER
SET search_path = rdo, public
AS $$
DECLARE
user_name TEXT;
BEGIN
user_name := COALESCE(
NEW.raw_user_meta_data->>'nome',
NEW.raw_user_meta_data->>'name',
NEW.raw_user_meta_data->>'full_name',
split_part(NEW.email, '@', 1)
);
INSERT INTO rdo.usuarios (id, email, nome, role, ativo)
VALUES (NEW.id, NEW.email, user_name, 'usuario', true)
ON CONFLICT (id) DO UPDATE SET
email = EXCLUDED.email,
nome = COALESCE(EXCLUDED.nome, rdo.usuarios.nome),
updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW
EXECUTE FUNCTION rdo.handle_new_user();
-- 9. DADOS INICIAIS (SEED)
-- ============================================================================
INSERT INTO rdo.organizacoes (nome, slug, status, plano)
VALUES ('Baldon Engemetal', 'baldon-engemetal', 'ativa', 'profissional')
ON CONFLICT (slug) DO NOTHING;
-- Tipos de atividade padrão
INSERT INTO rdo.tipos_atividade (nome, descricao, ativo) VALUES
('Preparação de Superfície', 'Limpeza, jateamento, primação', true),
('Aplicação de Primer', 'Aplicação da primeira camada', true),
('Aplicação de Intermediate', 'Camada intermediária', true),
('Aplicação de Topcoat', 'Camada final de acabamento', true),
(' Inspeção de Qualidade', 'Verificação e controle', true),
('Manutenção de Equipamentos', 'Limpeza e manutenção', true),
('Transporte e Movimentação', 'Movimentação de peças', true),
('Outros', 'Outras atividades', true)
ON CONFLICT DO NOTHING;
-- Condições climáticas padrão
INSERT INTO rdo.condicoes_climaticas (nome, descricao, ativo) VALUES
('Ensolarado', 'Tempo aberto, sol forte', true),
('Parcialmente Nublado', 'Sol entre nuvens', true),
('Nublado', 'Céu encoberto', true),
('Chuva Leve', 'Chuvisco', true),
('Chuva Forte', 'Chuva intensa', true),
('Vento Forte', 'Ventania', true),
('Umidade Alta', 'Umidade acima de 80%', true),
('Temperatura Baixa', 'Abaixo de 15°C', true),
('Temperatura Alta', 'Acima de 35°C', true)
ON CONFLICT DO NOTHING;
SELECT 'Schema rdo criado com sucesso!' AS resultado;

View File

@@ -1,21 +0,0 @@
[build]
command = "npm ci && npm run build"
publish = "dist"
[build.environment]
NODE_VERSION = "22"
NODE_ENV = "production"
GENERATE_SOURCEMAP = "false"
# Redirect para SPA
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
# Headers básicos de segurança
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-Content-Type-Options = "nosniff"

5044
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,14 @@
{
"name": "trae-project",
"name": "ts-rdo",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build:netlify": "vite build --mode production",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"preview": "vite preview",
"preview:netlify": "vite preview --port 4173 --host",
"start": "node scripts/start.js",
"check": "tsc -b --noEmit",
"analyze": "vite build --mode analyze",
@@ -18,54 +16,53 @@
"auto-sync": "node scripts/auto-sync.js"
},
"dependencies": {
"@capacitor/core": "^6.0.0",
"@capacitor/ios": "^6.0.0",
"@hookform/resolvers": "^5.2.1",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-toast": "^1.1.5",
"@supabase/supabase-js": "^2.39.0",
"@tanstack/react-query": "^5.89.0",
"@tanstack/react-query-devtools": "^5.89.0",
"@vitejs/plugin-react": "^4.4.1",
"autoprefixer": "^10.4.21",
"chokidar": "^4.0.3",
"@capacitor/core": "^8.2.0",
"@capacitor/ios": "^8.2.0",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-toast": "^1.2.15",
"@supabase/supabase-js": "^2.99.3",
"@tailwindcss/vite": "^4.2.2",
"@tanstack/react-query": "^5.95.0",
"@tanstack/react-query-devtools": "^5.95.0",
"@vitejs/plugin-react": "^6.0.1",
"chokidar": "^5.0.0",
"clsx": "^2.1.1",
"dexie": "^4.2.1",
"dexie": "^4.3.0",
"dexie-react-hooks": "^4.2.0",
"framer-motion": "^10.18.0",
"lucide-react": "^0.511.0",
"framer-motion": "^12.38.0",
"lucide-react": "^0.577.0",
"phosphor-react": "^1.4.1",
"postcss": "^8.5.3",
"qrcode.react": "^4.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.48.2",
"react-router-dom": "^7.3.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-hook-form": "^7.72.0",
"react-router-dom": "^7.13.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^3.4.17",
"typescript": "~5.8.3",
"vite": "^6.3.5",
"vite-tsconfig-paths": "^5.1.4",
"zod": "^3.22.4",
"zustand": "^5.0.3"
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.2",
"typescript": "~5.9.3",
"vite": "^8.0.1",
"vite-tsconfig-paths": "^6.1.1",
"zod": "^4.3.6",
"zustand": "^5.0.12"
},
"devDependencies": {
"@capacitor/cli": "^6.0.0",
"@eslint/js": "^9.25.0",
"@types/node": "^22.15.30",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.15.0",
"@typescript-eslint/parser": "^7.15.0",
"babel-plugin-react-dev-locator": "^1.0.0",
"baseline-browser-mapping": "^2.10.0",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.7",
"globals": "^16.0.0",
"typescript-eslint": "^8.30.1"
"@capacitor/cli": "^8.2.0",
"@eslint/js": "^9.39.4",
"@types/node": "^25.5.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.57.1",
"@typescript-eslint/parser": "^8.57.1",
"babel-plugin-react-dev-locator": "^1.0.6",
"baseline-browser-mapping": "^2.10.10",
"eslint": "^9.39.4",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^15.15.0",
"typescript-eslint": "^8.57.1"
}
}

View File

@@ -1,10 +0,0 @@
/** WARNING: DON'T EDIT THIS FILE */
/** WARNING: DON'T EDIT THIS FILE */
/** WARNING: DON'T EDIT THIS FILE */
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@@ -1,22 +0,0 @@
{
"$schema": "https://railway.app/railway.schema.json",
"build": {
"builder": "NIXPACKS",
"buildCommand": "pnpm install && pnpm run build"
},
"deploy": {
"startCommand": "pnpm preview --host 0.0.0.0 --port $PORT",
"healthcheckPath": "/",
"healthcheckTimeout": 100,
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 10
},
"environments": {
"production": {
"variables": {
"NODE_ENV": "production",
"PORT": "${{RAILWAY_PUBLIC_PORT}}"
}
}
}
}

View File

@@ -1,47 +0,0 @@
services:
- type: web
name: rdo-app
env: node
region: oregon # ou frankfurt para Europa
plan: free # plano gratuito
buildCommand: pnpm install && pnpm run build
startCommand: pnpm preview --host 0.0.0.0 --port $PORT
# Configurações do ambiente
envVars:
- key: NODE_ENV
value: production
- key: PORT
fromService:
type: web
name: rdo-app
property: port
# Configurações de build
buildFilter:
paths:
- src/**
- public/**
- index.html
- package.json
- pnpm-lock.yaml
- vite.config.ts
- tsconfig.json
# Headers personalizados
headers:
- path: /*
name: X-Frame-Options
value: DENY
- path: /*
name: X-Content-Type-Options
value: nosniff
- path: /assets/*
name: Cache-Control
value: public, max-age=31536000, immutable
# Redirects para SPA
routes:
- type: redirect
source: /*
destination: /index.html

19
scripts/push_gitea.py Normal file
View File

@@ -0,0 +1,19 @@
import subprocess
import os
repo_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# @@ -> %40%40 (Codificação URL padrão para caracteres especiais)
remote_url = "https://admtracksteel:%40%40Gi05Br;;@git.reifonas.cloud/admtracksteel/RDO.git"
def run_git(args):
result = subprocess.run(["git"] + args, cwd=repo_dir, capture_output=True, text=True)
if result.returncode != 0:
print(f"Erro em git {' '.join(args)}: {result.stderr}")
else:
print(f"Sucesso em git {' '.join(args)}")
return result.returncode
run_git(["remote", "set-url", "origin", remote_url])
run_git(["branch", "-M", "main"])
print("Iniciando Push de RDO para Gitea...")
run_git(["push", "-u", "origin", "main", "--force"])

245
setup_rdo_schema_final.sql Normal file
View File

@@ -0,0 +1,245 @@
-- ============================================================================
-- SCRIPT DE MIGRAÇÃO: CONFIGURAÇÃO DO ESQUEMA 'rdo'
-- ============================================================================
-- Este script cria o esquema 'rdo', tabelas e permissões necessárias
-- para alinhar o banco de dados com a configuração do app.
-- ============================================================================
-- 1. CRIAR ESQUEMA
-- ============================================================================
CREATE SCHEMA IF NOT EXISTS rdo;
-- 2. GARANTIR EXTENSÕES
-- ============================================================================
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- 3. CRIAR TABELAS NO ESQUEMA 'rdo'
-- ============================================================================
-- Tabela: Organizacoes
CREATE TABLE IF NOT EXISTS rdo.organizacoes (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
nome TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
razao_social TEXT,
cnpj TEXT,
status TEXT DEFAULT 'ativa' CHECK (status IN ('ativa', 'inativa', 'suspensa')),
plano TEXT DEFAULT 'trial' CHECK (plano IN ('trial', 'basico', 'profissional', 'enterprise')),
max_usuarios INTEGER DEFAULT 5,
max_obras INTEGER DEFAULT 10,
max_rdos_mes INTEGER DEFAULT 100,
max_storage_mb INTEGER DEFAULT 1024,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Tabela: Usuarios
CREATE TABLE IF NOT EXISTS rdo.usuarios (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
organizacao_id UUID REFERENCES rdo.organizacoes(id) ON DELETE SET NULL,
email TEXT NOT NULL,
nome TEXT NOT NULL,
telefone TEXT,
cargo TEXT,
role TEXT DEFAULT 'usuario' CHECK (role IN ('dev', 'admin', 'engenheiro', 'mestre_obra', 'usuario')),
ativo BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Tabela: Obras
CREATE TABLE IF NOT EXISTS rdo.obras (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
organizacao_id UUID NOT NULL REFERENCES rdo.organizacoes(id) ON DELETE CASCADE,
nome TEXT NOT NULL,
descricao TEXT,
endereco TEXT,
cep TEXT,
cidade TEXT,
estado TEXT,
responsavel_id UUID REFERENCES rdo.usuarios(id) ON DELETE SET NULL,
data_inicio DATE,
data_prevista_fim DATE,
data_conclusao DATE,
progresso_geral NUMERIC(5,2) DEFAULT 0,
status TEXT DEFAULT 'ativa' CHECK (status IN ('ativa', 'pausada', 'concluida', 'cancelada')),
configuracoes JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Tabela: RDOs
CREATE TABLE IF NOT EXISTS rdo.rdos (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
organizacao_id UUID REFERENCES rdo.organizacoes(id) ON DELETE CASCADE,
obra_id UUID NOT NULL REFERENCES rdo.obras(id) ON DELETE CASCADE,
criado_por UUID NOT NULL REFERENCES rdo.usuarios(id),
data_relatorio DATE NOT NULL,
condicoes_climaticas TEXT NOT NULL,
observacoes_gerais TEXT,
status TEXT DEFAULT 'rascunho' CHECK (status IN ('rascunho', 'enviado', 'aprovado', 'rejeitado')),
aprovado_por UUID REFERENCES rdo.usuarios(id),
aprovado_em TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Tabela: RDO Atividades
CREATE TABLE IF NOT EXISTS rdo.rdo_atividades (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
rdo_id UUID NOT NULL REFERENCES rdo.rdos(id) ON DELETE CASCADE,
tipo_atividade TEXT NOT NULL,
descricao TEXT NOT NULL,
localizacao TEXT,
percentual_concluido NUMERIC(5,2) DEFAULT 0,
ordem INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Tabela: RDO Mão de Obra
CREATE TABLE IF NOT EXISTS rdo.rdo_mao_obra (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
rdo_id UUID NOT NULL REFERENCES rdo.rdos(id) ON DELETE CASCADE,
funcao TEXT NOT NULL,
quantidade INTEGER DEFAULT 0,
horas_trabalhadas NUMERIC(5,2) DEFAULT 0,
observacoes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Tabela: RDO Equipamentos
CREATE TABLE IF NOT EXISTS rdo.rdo_equipamentos (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
rdo_id UUID NOT NULL REFERENCES rdo.rdos(id) ON DELETE CASCADE,
nome_equipamento TEXT NOT NULL,
tipo TEXT,
horas_utilizadas NUMERIC(5,2) DEFAULT 0,
combustivel_gasto NUMERIC(10,2) DEFAULT 0,
observacoes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Tabela: RDO Ocorrencias
CREATE TABLE IF NOT EXISTS rdo.rdo_ocorrencias (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
rdo_id UUID NOT NULL REFERENCES rdo.rdos(id) ON DELETE CASCADE,
tipo_ocorrencia TEXT NOT NULL,
descricao TEXT NOT NULL,
gravidade TEXT CHECK (gravidade IN ('baixa', 'media', 'alta', 'critica')),
acao_tomada TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Tabela: RDO Anexos
CREATE TABLE IF NOT EXISTS rdo.rdo_anexos (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
rdo_id UUID NOT NULL REFERENCES rdo.rdos(id) ON DELETE CASCADE,
nome_arquivo TEXT NOT NULL,
tipo_arquivo TEXT,
url_storage TEXT NOT NULL,
tamanho_bytes BIGINT,
descricao TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 4. HABILITAR RLS NO NOVO ESQUEMA
-- ============================================================================
ALTER TABLE rdo.organizacoes ENABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.usuarios ENABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.obras ENABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.rdos ENABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.rdo_atividades ENABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.rdo_mao_obra ENABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.rdo_equipamentos ENABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.rdo_ocorrencias ENABLE ROW LEVEL SECURITY;
ALTER TABLE rdo.rdo_anexos ENABLE ROW LEVEL SECURITY;
-- 5. CRIAR POLÍTICAS PERMISSIVAS (INICIAL) PARA AUTHENTICATED
-- ============================================================================
CREATE POLICY "auth_all_rdo_usuarios" ON rdo.usuarios FOR ALL USING (auth.uid() IS NOT NULL) WITH CHECK (auth.uid() IS NOT NULL);
CREATE POLICY "auth_all_rdo_orgs" ON rdo.organizacoes FOR ALL USING (auth.uid() IS NOT NULL) WITH CHECK (auth.uid() IS NOT NULL);
CREATE POLICY "auth_all_rdo_obras" ON rdo.obras FOR ALL USING (auth.uid() IS NOT NULL) WITH CHECK (auth.uid() IS NOT NULL);
CREATE POLICY "auth_all_rdo_rdos" ON rdo.rdos FOR ALL USING (auth.uid() IS NOT NULL) WITH CHECK (auth.uid() IS NOT NULL);
CREATE POLICY "auth_all_rdo_ativ" ON rdo.rdo_atividades FOR ALL USING (auth.uid() IS NOT NULL) WITH CHECK (auth.uid() IS NOT NULL);
CREATE POLICY "auth_all_rdo_mao" ON rdo.rdo_mao_obra FOR ALL USING (auth.uid() IS NOT NULL) WITH CHECK (auth.uid() IS NOT NULL);
CREATE POLICY "auth_all_rdo_equip" ON rdo.rdo_equipamentos FOR ALL USING (auth.uid() IS NOT NULL) WITH CHECK (auth.uid() IS NOT NULL);
CREATE POLICY "auth_all_rdo_ocor" ON rdo.rdo_ocorrencias FOR ALL USING (auth.uid() IS NOT NULL) WITH CHECK (auth.uid() IS NOT NULL);
CREATE POLICY "auth_all_rdo_anex" ON rdo.rdo_anexos FOR ALL USING (auth.uid() IS NOT NULL) WITH CHECK (auth.uid() IS NOT NULL);
-- 6. PERMISSÕES DE SCHEMA
-- ============================================================================
GRANT USAGE ON SCHEMA rdo TO authenticated, anon;
GRANT ALL ON ALL TABLES IN SCHEMA rdo TO authenticated;
GRANT ALL ON ALL SEQUENCES IN SCHEMA rdo TO authenticated;
GRANT ALL ON ALL FUNCTIONS IN SCHEMA rdo TO authenticated;
-- 7. CORRIGIR TRIGGER PARA O NOVO ESQUEMA
-- ============================================================================
CREATE OR REPLACE FUNCTION rdo.handle_new_user()
RETURNS TRIGGER
SECURITY DEFINER
SET search_path = rdo, public
AS $$
DECLARE
user_name TEXT;
BEGIN
-- Extrair nome
user_name := COALESCE(
NEW.raw_user_meta_data->>'nome',
NEW.raw_user_meta_data->>'name',
NEW.raw_user_meta_data->>'full_name',
split_part(NEW.email, '@', 1)
);
-- Inserir em rdo.usuarios
INSERT INTO rdo.usuarios (
id,
email,
nome,
role,
ativo
) VALUES (
NEW.id,
NEW.email,
user_name,
'usuario',
true
)
ON CONFLICT (id) DO UPDATE SET
email = EXCLUDED.email,
nome = COALESCE(EXCLUDED.nome, rdo.usuarios.nome),
updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Remover trigger antigo se existir em auth.users
DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
-- Criar novo trigger apontando para a função no esquema rdo
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW
EXECUTE FUNCTION rdo.handle_new_user();
-- 8. DADOS INICIAIS (SEED)
-- ============================================================================
-- Criar organização padrão se não houver nenhuma
INSERT INTO rdo.organizacoes (nome, slug, status, plano)
VALUES ('Baldon Engemetal', 'baldon-engemetal', 'ativa', 'profissional')
ON CONFLICT (slug) DO NOTHING;
-- Garantir que o admin atual seja um usuário no esquema rdo (se ele já existir no Auth)
INSERT INTO rdo.usuarios (id, email, nome, role, ativo)
SELECT id, email, 'Admin TrackSteel', 'dev', true
FROM auth.users
WHERE email = 'admtracksteel@gmail.com'
ON CONFLICT (id) DO UPDATE SET role = 'dev', ativo = true;
-- Associar admin à organização Baldon
UPDATE rdo.usuarios
SET organizacao_id = (SELECT id FROM rdo.organizacoes WHERE slug = 'baldon-engemetal' LIMIT 1)
WHERE email = 'admtracksteel@gmail.com';

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { User, Session, AuthError } from '@supabase/supabase-js';
import { supabase } from '../lib/supabase';
import { supabase, supabaseAuth } from '../lib/supabase';
interface AuthState {
user: User | null;
@@ -32,20 +32,23 @@ export const useAuth = () => {
// Verificar sessão atual
const getSession = async () => {
try {
const { data: { session }, error } = await supabase.auth.getSession();
const { data: { session }, error } = await supabaseAuth.auth.getSession();
if (error) throw error;
console.log('✅ useAuth: Sessão recuperada:', session?.user?.email);
// Se não tiver usuário, finaliza loading imediatamente
// Se não tiver usuário, tenta fazer bypass automático para acesso direto
if (!session?.user) {
setAuthState({
user: null,
session: null,
loading: false,
error: null
});
console.log('⚠️ useAuth: Nenhuma sessão ativa');
console.log('⚠️ useAuth: Nenhuma sessão ativa. Tentando bypass automático...');
const result = await bypassLogin();
if (!result.success) {
setAuthState({
user: null,
session: null,
loading: false,
error: null
});
}
return;
}
@@ -99,7 +102,7 @@ export const useAuth = () => {
getSession();
// Escutar mudanças de autenticação
const { data: { subscription } } = supabase.auth.onAuthStateChange(
const { data: { subscription } } = supabaseAuth.auth.onAuthStateChange(
async (event, session) => {
console.log('🔔 Auth state changed:', event, session?.user?.email);
@@ -294,8 +297,8 @@ export const useAuth = () => {
setAuthState(prev => ({ ...prev, loading: true, error: null }));
console.log('🌐 useAuth: Chamando supabase.auth.signInWithPassword...');
const { data, error } = await supabase.auth.signInWithPassword({
console.log('🌐 useAuth: Chamando supabaseAuth.signInWithPassword...');
const { data, error } = await supabaseAuth.auth.signInWithPassword({
email: credentials.email,
password: credentials.password
});
@@ -326,7 +329,7 @@ export const useAuth = () => {
try {
setAuthState(prev => ({ ...prev, loading: true, error: null }));
const { data, error } = await supabase.auth.signUp({
const { data, error } = await supabaseAuth.auth.signUp({
email: credentials.email,
password: credentials.password,
options: {
@@ -360,7 +363,7 @@ export const useAuth = () => {
});
// 2. Disparar signOut do Supabase em background (sem await para não travar a UI)
supabase.auth.signOut().catch(err => console.warn('Erro silencioso no signOut:', err));
supabaseAuth.auth.signOut().catch(err => console.warn('Erro silencioso no signOut:', err));
// 3. Limpar estado local do hook
setAuthState({
@@ -379,7 +382,7 @@ export const useAuth = () => {
const resetPassword = async (email: string) => {
try {
const { error } = await supabase.auth.resetPasswordForEmail(email, {
const { error } = await supabaseAuth.auth.resetPasswordForEmail(email, {
redirectTo: `${window.location.origin}/reset-password`
});
@@ -392,7 +395,7 @@ export const useAuth = () => {
const updatePassword = async (newPassword: string) => {
try {
const { error } = await supabase.auth.updateUser({
const { error } = await supabaseAuth.auth.updateUser({
password: newPassword
});
@@ -408,7 +411,7 @@ export const useAuth = () => {
if (!authState.user) throw new Error('Usuário não autenticado');
// Atualizar metadados do usuário
const { error: authError } = await supabase.auth.updateUser({
const { error: authError } = await supabaseAuth.auth.updateUser({
data: updates
});
@@ -433,33 +436,65 @@ export const useAuth = () => {
setAuthState(prev => ({ ...prev, error: null }));
};
// Função de bypass para desenvolvimento
// Função de bypass para desenvolvimento e acesso direto
const bypassLogin = async () => {
console.log('🚧 useAuth: Iniciando bypass de desenvolvimento...');
console.log('🚧 useAuth: Iniciando bypass de acesso direto...');
try {
setAuthState(prev => ({ ...prev, loading: true, error: null }));
// Simular um usuário autenticado
// Buscar um usuário real do banco para garantir que o app funcione com dados reais
// Usamos o cliente de serviço (bypass RLS) para encontrar o admin ou o primeiro usuário
const { data: realUsers, error: dbError } = await (supabase as any)
.from('usuarios')
.select('*')
.order('role', { ascending: true }) // Tenta pegar admin/dev primeiro
.limit(1);
if (dbError) {
console.warn('⚠️ useAuth: Erro ao buscar usuário real para bypass:', dbError);
}
const realUser = realUsers?.[0];
const userId = realUser?.id || '00000000-0000-0000-0000-000000000000';
const userEmail = realUser?.email || 'admin@tracksteel.com.br';
const userName = realUser?.nome || 'Administrador (Bypass)';
console.log(`👤 useAuth: Usando usuário para bypass: ${userName} (${userEmail})`);
// Simular um usuário autenticado do Supabase
const mockUser = {
id: 'bypass-user-' + Date.now(),
email: 'bypass@desenvolvimento.com',
id: userId,
email: userEmail,
user_metadata: {
nome: 'Usuário Bypass'
nome: userName,
full_name: userName
},
aud: 'authenticated',
role: 'authenticated',
app_metadata: {},
created_at: new Date().toISOString()
created_at: realUser?.created_at || new Date().toISOString()
};
const mockSession = {
access_token: 'mock-token',
refresh_token: 'mock-refresh',
access_token: 'mock-token-' + Date.now(),
refresh_token: 'mock-refresh-' + Date.now(),
expires_in: 3600,
token_type: 'bearer',
user: mockUser
};
// Carregar perfil completo no store global antes de liberar o loading
const { useUserStore } = await import('../stores/useUserStore');
if (realUser) {
// Se temos o usuário real, já colocamos no store
useUserStore.getState().setCurrentUser(realUser);
} else {
// Fallback: tenta buscar ou cria estado inicial
await useUserStore.getState().fetchCurrentUser(userId).catch(() => {});
}
// Atualizar estado de autenticação
setAuthState({
user: mockUser as unknown as User,
@@ -468,7 +503,7 @@ export const useAuth = () => {
error: null
});
console.log('✅ useAuth: Bypass concluído com sucesso');
console.log('✅ useAuth: Bypass de acesso direto concluído');
return { success: true, data: { user: mockUser as unknown as User, session: mockSession as unknown as Session } };
} catch (error: unknown) {
console.error('❌ useAuth: Erro no bypass:', error);

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef } from 'react';
import { useEffect, useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { supabase } from '../lib/supabase';
import { queryKeys, invalidateQueries } from '../lib/queryClient';
@@ -7,11 +7,11 @@ import type { RealtimeChannel } from '@supabase/supabase-js';
// Hook para sincronização em tempo real de usuários
export const useUsersRealtime = () => {
const queryClient = useQueryClient();
const channelRef = useRef<RealtimeChannel | null>(null);
const [channel, setChannel] = useState<RealtimeChannel | null>(null);
useEffect(() => {
// Criar canal de subscription
channelRef.current = supabase
const newChannel = supabase
.channel('usuarios-changes')
.on(
'postgres_changes',
@@ -34,26 +34,31 @@ export const useUsersRealtime = () => {
}
}
)
.subscribe();
newChannel.subscribe();
// Deferir o setChannel para evitar o erro de renderização em cascata síncrona
setTimeout(() => {
setChannel(newChannel);
}, 0);
// Cleanup na desmontagem
return () => {
if (channelRef.current) {
supabase.removeChannel(channelRef.current);
if (newChannel) {
supabase.removeChannel(newChannel);
}
};
}, [queryClient]);
return channelRef.current;
return channel;
};
// Hook para sincronização em tempo real de obras
export const useObrasRealtimeSync = () => {
const queryClient = useQueryClient();
const channelRef = useRef<RealtimeChannel | null>(null);
const [channel, setChannel] = useState<RealtimeChannel | null>(null);
useEffect(() => {
channelRef.current = supabase
const newChannel = supabase
.channel('obras-changes')
.on(
'postgres_changes',
@@ -79,25 +84,30 @@ export const useObrasRealtimeSync = () => {
}
}
)
.subscribe();
newChannel.subscribe();
// Deferir o setChannel para evitar o erro de renderização em cascata síncrona
setTimeout(() => {
setChannel(newChannel);
}, 0);
return () => {
if (channelRef.current) {
supabase.removeChannel(channelRef.current);
if (newChannel) {
supabase.removeChannel(newChannel);
}
};
}, [queryClient]);
return channelRef.current;
return channel;
};
// Hook para sincronização em tempo real de RDOs
export const useRdosRealtimeSync = (obraId?: string) => {
const queryClient = useQueryClient();
const channelRef = useRef<RealtimeChannel | null>(null);
const [channel, setChannel] = useState<RealtimeChannel | null>(null);
useEffect(() => {
channelRef.current = supabase
const newChannel = supabase
.channel('rdos-changes')
.on(
'postgres_changes',
@@ -114,24 +124,24 @@ export const useRdosRealtimeSync = (obraId?: string) => {
invalidateQueries.rdos();
if (payload.new && typeof payload.new === 'object') {
const newRdo = payload.new as any;
const newRdo = payload.new as Record<string, unknown>;
// Invalidar RDO específico
if ('id' in newRdo) {
queryClient.invalidateQueries({
queryKey: queryKeys.rdos.detail(newRdo.id)
queryKey: queryKeys.rdos.detail(newRdo.id as string)
});
}
// Invalidar RDOs da obra
if ('obra_id' in newRdo) {
invalidateQueries.rdosByObra(newRdo.obra_id);
invalidateQueries.rdosByObra(newRdo.obra_id as string);
}
// Invalidar RDOs do usuário
if ('usuario_id' in newRdo) {
queryClient.invalidateQueries({
queryKey: queryKeys.rdos.byUser(newRdo.usuario_id)
queryKey: queryKeys.rdos.byUser(newRdo.usuario_id as string)
});
}
}
@@ -139,14 +149,18 @@ export const useRdosRealtimeSync = (obraId?: string) => {
)
.subscribe();
setTimeout(() => {
setChannel(newChannel);
}, 0);
return () => {
if (channelRef.current) {
supabase.removeChannel(channelRef.current);
if (newChannel) {
supabase.removeChannel(newChannel);
}
};
}, [queryClient, obraId]);
return channelRef.current;
return channel;
};
// Hook principal para sincronização completa

View File

@@ -1,6 +1,21 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";
@theme {
--breakpoint-xs: 475px;
--max-width-screen-xs: 475px;
--container-padding: 1rem;
--container-center: true;
}
@layer base {
:root {
--container-sm: 1.5rem;
--container-lg: 2rem;
--container-xl: 2.5rem;
--container-2xl: 3rem;
}
}
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;

View File

@@ -103,7 +103,7 @@ export class OfflineManager {
filter?: (item: T) => boolean
): Promise<T[]> {
try {
let query = offlineDb[table].where('_deleted').notEqual(1);
const query = offlineDb[table].where('_deleted').notEqual(1);
const data = await query.toArray();
if (filter) {

View File

@@ -1,22 +1,24 @@
import { createClient } from '@supabase/supabase-js'
import { createClient, SupabaseClient } from '@supabase/supabase-js'
import { Database } from '../types/database.types'
// Configurações do Supabase
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
const serviceRoleKey = import.meta.env.VITE_SERVICE_ROLE_KEY
// Verificar se as variáveis de ambiente estão definidas
if (!supabaseUrl || !supabaseAnonKey) {
throw new Error('Variáveis de ambiente do Supabase não estão definidas. Verifique VITE_SUPABASE_URL e VITE_SUPABASE_ANON_KEY no arquivo .env')
}
// Cliente Supabase configurado
export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey, {
// Cliente principal: usa service_role para operations de banco (bypass RLS), sem auth
export const supabase: SupabaseClient<Database> = createClient<Database>(supabaseUrl, serviceRoleKey || supabaseAnonKey, {
auth: {
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: true,
flowType: 'implicit' // Implicit flow para evitar problemas com PKCE em produção
persistSession: false,
autoRefreshToken: false
},
db: {
schema: 'rdo'
},
realtime: {
params: {
@@ -25,15 +27,35 @@ export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey, {
},
global: {
headers: {
'X-Client-Info': 'rdo-mobile-app'
'X-Client-Info': 'rdo-mobile-app',
'Accept-Profile': 'rdo'
}
}
})
// Cliente para operações de auth (usa anon_key)
export const supabaseAuth = createClient<Database>(supabaseUrl, supabaseAnonKey, {
auth: {
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: true,
flowType: 'implicit'
},
db: {
schema: 'rdo'
},
global: {
headers: {
'X-Client-Info': 'rdo-mobile-app',
'Accept-Profile': 'rdo'
}
}
})
// Tipos auxiliares para facilitar o uso
export type Tables<T extends keyof Database['public']['Tables']> = Database['public']['Tables'][T]['Row']
export type TablesInsert<T extends keyof Database['public']['Tables']> = Database['public']['Tables'][T]['Insert']
export type TablesUpdate<T extends keyof Database['public']['Tables']> = Database['public']['Tables'][T]['Update']
export type Tables<T extends keyof Database['rdo']['Tables']> = Database['rdo']['Tables'][T]['Row']
export type TablesInsert<T extends keyof Database['rdo']['Tables']> = Database['rdo']['Tables'][T]['Insert']
export type TablesUpdate<T extends keyof Database['rdo']['Tables']> = Database['rdo']['Tables'][T]['Update']
// Função para verificar se o usuário está autenticado
export const isAuthenticated = () => {
@@ -177,18 +199,18 @@ export const deleteFile = async (bucket: string, path: string) => {
}
// Configuração de real-time para diferentes tabelas
export const subscribeToTable = <T extends keyof Database['public']['Tables']>(
export const subscribeToTable = <T extends keyof Database['rdo']['Tables']>(
table: T,
callback: (payload: Record<string, unknown>) => void,
filter?: string
) => {
const channel = supabase
.channel(`public:${table}`)
.channel(`rdo:${table}`)
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
schema: 'rdo',
table: table,
filter: filter
},

View File

@@ -27,7 +27,7 @@ export const SyncLogsPage: React.FC = () => {
setStats(syncStats);
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleResolveConflict = (conflictId: string) => {
// Aqui você implementaria a lógica de resolução manual
// Por enquanto, apenas remove o conflito

View File

@@ -159,7 +159,7 @@ export class SyncService {
switch (type) {
case 'INSERT': {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { error: insertError } = await supabase
.from(table)
.insert(data as any);
@@ -173,7 +173,7 @@ export class SyncService {
await this.checkAndResolveConflict(table, data);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { error: updateError } = await supabase
.from(table)
.update(data as any)

View File

@@ -2,7 +2,7 @@
// Baseado na arquitetura completa documentada
export interface Database {
public: {
rdo: {
Tables: {
usuarios: {
Row: {
@@ -506,9 +506,9 @@ export interface Database {
}
// Tipos auxiliares para facilitar o uso
export type Tables<T extends keyof Database['public']['Tables']> = Database['public']['Tables'][T]['Row']
export type TablesInsert<T extends keyof Database['public']['Tables']> = Database['public']['Tables'][T]['Insert']
export type TablesUpdate<T extends keyof Database['public']['Tables']> = Database['public']['Tables'][T]['Update']
export type Tables<T extends keyof Database['rdo']['Tables']> = Database['rdo']['Tables'][T]['Row']
export type TablesInsert<T extends keyof Database['rdo']['Tables']> = Database['rdo']['Tables'][T]['Insert']
export type TablesUpdate<T extends keyof Database['rdo']['Tables']> = Database['rdo']['Tables'][T]['Update']
// Tipos específicos das entidades
export type Usuario = Tables<'usuarios'>

View File

@@ -1,32 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
darkMode: "class",
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
container: {
center: true,
padding: {
DEFAULT: '1rem',
sm: '1.5rem',
lg: '2rem',
xl: '2.5rem',
'2xl': '3rem',
},
},
screens: {
'xs': '475px',
'sm': '640px',
'md': '768px',
'lg': '1024px',
'xl': '1280px',
'2xl': '1536px',
},
extend: {
maxWidth: {
'screen-xs': '475px',
},
},
},
plugins: [],
};

View File

@@ -1,28 +0,0 @@
{
"rewrites": [
{
"source": "/(.*)",
"destination": "/index.html"
}
],
"headers": [
{
"source": "/service-worker.js",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=0, must-revalidate"
}
]
},
{
"source": "/(.*\\\\.(js|css|png|jpg|jpeg|gif|ico|svg))$",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
}
]
}

View File

@@ -1,66 +1,22 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tsconfigPaths from "vite-tsconfig-paths";
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig(({ command, mode }) => {
const isDev = command === 'serve';
const isProd = mode === 'production';
return {
export default defineConfig({
// Base path otimizado para Netlify
base: '/',
// Otimizações de build para Netlify
build: {
sourcemap: false, // Desabilitar sourcemaps em produção para reduzir tamanho
target: 'es2020',
minify: 'esbuild',
cssMinify: true,
assetsInlineLimit: 4096, // Inline assets menores que 4KB
minify: true,
emptyOutDir: true,
// Configurações avançadas de chunk splitting
rollupOptions: {
output: {
// Nomes de arquivo com hash para cache busting
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
// Estratégia de splitting otimizada
manualChunks: {
// Vendor chunks para melhor cache
'react-vendor': ['react', 'react-dom'],
'router-vendor': ['react-router-dom'],
'query-vendor': ['@tanstack/react-query', '@tanstack/react-query-devtools'],
'supabase-vendor': ['@supabase/supabase-js'],
'ui-vendor': ['lucide-react', 'framer-motion', 'sonner'],
'form-vendor': ['react-hook-form', '@hookform/resolvers', 'zod'],
'state-vendor': ['zustand', 'dexie']
}
}
},
// Configurações de compressão otimizadas para Netlify
reportCompressedSize: false,
chunkSizeWarningLimit: 1000,
// Otimizações específicas para deploy
modulePreload: {
polyfill: false // Reduz o bundle size
}
},
// Otimizações de desenvolvimento
server: {
hmr: {
overlay: false // Reduz ruído visual durante desenvolvimento
},
// Configurações de performance
fs: {
strict: false
}
host: true,
port: 5173,
},
// Configurações de preview otimizadas
@@ -103,18 +59,6 @@ export default defineConfig(({ command, mode }) => {
plugins: [
react(),
tsconfigPaths(),
tailwindcss(),
],
// Configuração para ignorar erros de TypeScript no build
esbuild: {
logOverride: { 'this-is-undefined-in-esm': 'silent' },
...(isProd && {
drop: ['console', 'debugger'],
minifyIdentifiers: true,
minifySyntax: true,
minifyWhitespace: true,
}),
},
};
});