primeiras_alteracoes

This commit is contained in:
2026-04-03 19:58:41 +00:00
parent bc70aaaa5c
commit d5a2b570ec
3 changed files with 313 additions and 17 deletions

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,6 +1,6 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { User, Session, AuthError } from '@supabase/supabase-js'; import { User, Session, AuthError } from '@supabase/supabase-js';
import { supabase } from '../lib/supabase'; import { supabase, supabaseAuth } from '../lib/supabase';
interface AuthState { interface AuthState {
user: User | null; user: User | null;
@@ -32,7 +32,7 @@ export const useAuth = () => {
// Verificar sessão atual // Verificar sessão atual
const getSession = async () => { const getSession = async () => {
try { try {
const { data: { session }, error } = await supabase.auth.getSession(); const { data: { session }, error } = await supabaseAuth.getSession();
if (error) throw error; if (error) throw error;
console.log('✅ useAuth: Sessão recuperada:', session?.user?.email); console.log('✅ useAuth: Sessão recuperada:', session?.user?.email);
@@ -99,7 +99,7 @@ export const useAuth = () => {
getSession(); getSession();
// Escutar mudanças de autenticação // Escutar mudanças de autenticação
const { data: { subscription } } = supabase.auth.onAuthStateChange( const { data: { subscription } } = supabaseAuth.onAuthStateChange(
async (event, session) => { async (event, session) => {
console.log('🔔 Auth state changed:', event, session?.user?.email); console.log('🔔 Auth state changed:', event, session?.user?.email);
@@ -294,8 +294,8 @@ export const useAuth = () => {
setAuthState(prev => ({ ...prev, loading: true, error: null })); setAuthState(prev => ({ ...prev, loading: true, error: null }));
console.log('🌐 useAuth: Chamando supabase.auth.signInWithPassword...'); console.log('🌐 useAuth: Chamando supabaseAuth.signInWithPassword...');
const { data, error } = await supabase.auth.signInWithPassword({ const { data, error } = await supabaseAuth.signInWithPassword({
email: credentials.email, email: credentials.email,
password: credentials.password password: credentials.password
}); });
@@ -326,7 +326,7 @@ export const useAuth = () => {
try { try {
setAuthState(prev => ({ ...prev, loading: true, error: null })); setAuthState(prev => ({ ...prev, loading: true, error: null }));
const { data, error } = await supabase.auth.signUp({ const { data, error } = await supabaseAuth.signUp({
email: credentials.email, email: credentials.email,
password: credentials.password, password: credentials.password,
options: { options: {
@@ -360,7 +360,7 @@ export const useAuth = () => {
}); });
// 2. Disparar signOut do Supabase em background (sem await para não travar a UI) // 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.signOut().catch(err => console.warn('Erro silencioso no signOut:', err));
// 3. Limpar estado local do hook // 3. Limpar estado local do hook
setAuthState({ setAuthState({
@@ -379,7 +379,7 @@ export const useAuth = () => {
const resetPassword = async (email: string) => { const resetPassword = async (email: string) => {
try { try {
const { error } = await supabase.auth.resetPasswordForEmail(email, { const { error } = await supabaseAuth.resetPasswordForEmail(email, {
redirectTo: `${window.location.origin}/reset-password` redirectTo: `${window.location.origin}/reset-password`
}); });
@@ -392,7 +392,7 @@ export const useAuth = () => {
const updatePassword = async (newPassword: string) => { const updatePassword = async (newPassword: string) => {
try { try {
const { error } = await supabase.auth.updateUser({ const { error } = await supabaseAuth.updateUser({
password: newPassword password: newPassword
}); });
@@ -408,7 +408,7 @@ export const useAuth = () => {
if (!authState.user) throw new Error('Usuário não autenticado'); if (!authState.user) throw new Error('Usuário não autenticado');
// Atualizar metadados do usuário // Atualizar metadados do usuário
const { error: authError } = await supabase.auth.updateUser({ const { error: authError } = await supabaseAuth.updateUser({
data: updates data: updates
}); });

View File

@@ -1,22 +1,23 @@
import { createClient } from '@supabase/supabase-js' import { createClient, SupabaseClient } from '@supabase/supabase-js'
import { Database } from '../types/database.types' import { Database } from '../types/database.types'
// Configurações do Supabase // Configurações do Supabase
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY 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 // Verificar se as variáveis de ambiente estão definidas
if (!supabaseUrl || !supabaseAnonKey) { 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') 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 // Cliente Supabase configurado para leitura (usa service_role para bypassing RLS)
export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey, { export const supabase: SupabaseClient<Database> = createClient<Database>(supabaseUrl, serviceRoleKey || supabaseAnonKey, {
auth: { auth: {
autoRefreshToken: true, autoRefreshToken: true,
persistSession: true, persistSession: true,
detectSessionInUrl: true, detectSessionInUrl: true,
flowType: 'implicit' // Implicit flow para evitar problemas com PKCE em produção flowType: 'implicit'
}, },
db: { db: {
schema: 'rdo' schema: 'rdo'
@@ -28,7 +29,27 @@ export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey, {
}, },
global: { global: {
headers: { 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'
} }
} }
}) })
@@ -180,13 +201,13 @@ export const deleteFile = async (bucket: string, path: string) => {
} }
// Configuração de real-time para diferentes tabelas // 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, table: T,
callback: (payload: Record<string, unknown>) => void, callback: (payload: Record<string, unknown>) => void,
filter?: string filter?: string
) => { ) => {
const channel = supabase const channel = supabase
.channel(`public:${table}`) .channel(`rdo:${table}`)
.on( .on(
'postgres_changes', 'postgres_changes',
{ {