First commit - backup RDOC

This commit is contained in:
2026-02-20 07:20:32 -03:00
commit b7415f0586
259 changed files with 51707 additions and 0 deletions

12
src/hooks/index.ts Normal file
View File

@@ -0,0 +1,12 @@
// Exportar hooks de queries
export * from './queries';
// Exportar hooks offline
export * from './useOffline';
export * from './useRealtimeSync';
// Exportar hooks de autenticação
export * from './useAuth';
// Exportar hook de convites
export * from './useInviteCode';

View File

@@ -0,0 +1,7 @@
// Exportar todos os hooks de queries
export * from './useUsers';
export * from './useObras';
export * from './useRdos';
// Re-exportar utilitários do queryClient
// export { queryKeys, invalidateQueries } from '../../lib/queryClient'; // Comentado temporariamente

View File

@@ -0,0 +1,320 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '../../lib/supabase';
// import { queryKeys, invalidateQueries } from '../../lib/queryClient'; // Comentado temporariamente
import type { Obra, ObraInsert, ObraUpdate } from '../../types/database.types';
// Hook para buscar todas as obras
export const useObras = (filters?: {
status?: string;
responsavel_id?: string;
ativo?: boolean;
search?: string;
}) => {
return useQuery({
queryKey: ['obras', 'list', filters || {}],
queryFn: async (): Promise<Obra[]> => {
let query = (supabase as any)
.from('obras')
.select(`
*,
responsavel:usuarios!obras_responsavel_id_fkey(id, nome, email),
cliente:clientes(id, nome, email)
`)
.order('created_at', { ascending: false });
if (filters?.status) {
query = query.eq('status', filters.status);
}
if (filters?.responsavel_id) {
query = query.eq('responsavel_id', filters.responsavel_id);
}
if (filters?.ativo !== undefined) {
query = query.eq('ativo', filters.ativo);
}
if (filters?.search) {
query = query.or(`nome.ilike.%${filters.search}%,descricao.ilike.%${filters.search}%`);
}
const { data, error } = await query;
if (error) {
throw new Error(`Erro ao buscar obras: ${error.message}`);
}
return data || [];
},
staleTime: 3 * 60 * 1000, // 3 minutos
});
};
// Hook para buscar obra por ID
export const useObra = (id: string | undefined) => {
return useQuery({
queryKey: ['obras', 'detail', id || ''],
queryFn: async (): Promise<Obra | null> => {
if (!id) return null;
const { data, error } = await supabase
.from('obras')
.select(`
*,
responsavel:usuarios!obras_responsavel_id_fkey(id, nome, email, telefone),
cliente:clientes(id, nome, email, telefone, endereco),
rdos:rdos(id, data, status, created_at)
`)
.eq('id', id)
.single();
if (error) {
if (error.code === 'PGRST116') {
return null; // Obra não encontrada
}
throw new Error(`Erro ao buscar obra: ${error.message}`);
}
return data;
},
enabled: !!id,
staleTime: 5 * 60 * 1000, // 5 minutos
});
};
// Hook para buscar obras do usuário
export const useUserObras = (userId: string | undefined) => {
return useQuery({
queryKey: ['obras', 'byUser', userId || ''],
queryFn: async (): Promise<Obra[]> => {
if (!userId) return [];
const { data, error } = await supabase
.from('obras')
.select(`
*,
responsavel:usuarios!obras_responsavel_id_fkey(id, nome, email),
cliente:clientes(id, nome, email)
`)
.eq('responsavel_id', userId)
.eq('ativo', true)
.order('created_at', { ascending: false });
if (error) {
throw new Error(`Erro ao buscar obras do usuário: ${error.message}`);
}
return data || [];
},
enabled: !!userId,
staleTime: 5 * 60 * 1000, // 5 minutos
});
};
// Hook para criar obra
export const useCreateObra = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (obraData: ObraInsert): Promise<Obra> => {
const { data, error } = await supabase
.from('obras')
.insert(obraData as any)
.select(`
*,
responsavel:usuarios!obras_responsavel_id_fkey(id, nome, email),
cliente:clientes(id, nome, email)
`)
.single();
if (error) {
throw new Error(`Erro ao criar obra: ${error.message}`);
}
return data;
},
onSuccess: (newObra) => {
// Invalidar cache de obras
queryClient.invalidateQueries({ queryKey: ['obras'] });
// Adicionar a nova obra ao cache
queryClient.setQueryData(
['obras', 'detail', newObra.id],
newObra
);
// Invalidar obras do responsável
if (newObra.responsavel_id) {
queryClient.invalidateQueries({
queryKey: ['obras', 'byUser', newObra.responsavel_id]
});
}
},
onError: (error) => {
console.error('Erro ao criar obra:', error);
},
});
};
// Hook para atualizar obra
export const useUpdateObra = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, updates }: { id: string; updates: ObraUpdate }): Promise<Obra> => {
const { data, error } = await (supabase as any)
.from('obras')
.update(updates)
.eq('id', id)
.select(`
*,
responsavel:usuarios!obras_responsavel_id_fkey(id, nome, email),
cliente:clientes(id, nome, email)
`)
.single();
if (error) {
throw new Error(`Erro ao atualizar obra: ${error.message}`);
}
return data;
},
onSuccess: (updatedObra) => {
// Invalidar cache de obras
queryClient.invalidateQueries({ queryKey: ['obras'] });
// Atualizar a obra específica no cache
queryClient.setQueryData(
['obras', 'detail', updatedObra.id],
updatedObra
);
// Invalidar obras do responsável
if (updatedObra.responsavel_id) {
queryClient.invalidateQueries({
queryKey: ['obras', 'byUser', updatedObra.responsavel_id]
});
}
},
onError: (error) => {
console.error('Erro ao atualizar obra:', error);
},
});
};
// Hook para deletar obra (soft delete)
export const useDeleteObra = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (id: string): Promise<void> => {
const { error } = await (supabase as any)
.from('obras')
.update({ ativo: false, deleted_at: new Date().toISOString() })
.eq('id', id);
if (error) {
throw new Error(`Erro ao deletar obra: ${error.message}`);
}
},
onSuccess: (_, deletedId) => {
// Invalidar cache de obras
queryClient.invalidateQueries({ queryKey: ['obras'] });
// Remover a obra específica do cache
queryClient.removeQueries({
queryKey: ['obras', 'detail', deletedId]
});
// Invalidar RDOs da obra
queryClient.invalidateQueries({ queryKey: ['rdos', 'byObra', deletedId] });
},
onError: (error) => {
console.error('Erro ao deletar obra:', error);
},
});
};
// Hook para alterar status da obra
export const useUpdateObraStatus = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, status }: { id: string; status: 'ativa' | 'pausada' | 'concluida' | 'cancelada' }): Promise<Obra> => {
const updates: ObraUpdate = { status };
// Se estiver finalizando, adicionar data de conclusão
if (status === 'concluida') {
updates.data_conclusao = new Date().toISOString();
}
const { data, error } = await (supabase as any)
.from('obras')
.update(updates)
.eq('id', id)
.select(`
*,
responsavel:usuarios!obras_responsavel_id_fkey(id, nome, email),
cliente:clientes(id, nome, email)
`)
.single();
if (error) {
throw new Error(`Erro ao alterar status da obra: ${error.message}`);
}
return data;
},
onSuccess: (updatedObra) => {
// Invalidar cache de obras
queryClient.invalidateQueries({ queryKey: ['obras'] });
// Atualizar a obra específica no cache
queryClient.setQueryData(
['obras', 'detail', updatedObra.id],
updatedObra
);
// Invalidar obras do responsável
if (updatedObra.responsavel_id) {
queryClient.invalidateQueries({
queryKey: ['obras', 'byUser', updatedObra.responsavel_id]
});
}
},
onError: (error) => {
console.error('Erro ao alterar status da obra:', error);
},
});
};
// Hook para estatísticas da obra
export const useObraStats = (id: string | undefined) => {
return useQuery({
queryKey: ['obras', 'detail', id || '', 'stats'],
queryFn: async () => {
if (!id) return null;
// Buscar estatísticas dos RDOs da obra
const { data: rdosStats, error: rdosError } = await supabase
.from('rdos')
.select('status')
.eq('obra_id', id);
if (rdosError) {
throw new Error(`Erro ao buscar estatísticas: ${rdosError.message}`);
}
const stats = {
total_rdos: rdosStats?.length || 0,
rdos_pendentes: rdosStats?.filter((r: any) => r.status === 'pendente').length || 0,
rdos_aprovados: rdosStats?.filter((r: any) => r.status === 'aprovado').length || 0,
rdos_rejeitados: rdosStats?.filter((r: any) => r.status === 'rejeitado').length || 0,
};
return stats;
},
enabled: !!id,
staleTime: 2 * 60 * 1000, // 2 minutos
});
};

View File

@@ -0,0 +1,435 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '../../lib/supabase';
// import { queryKeys, invalidateQueries } from '../../lib/queryClient';
import type { RDO, RDOInsert, RDOUpdate } from '../../types/database.types';
// Hook para buscar todos os RDOs
export const useRdos = (filters?: {
obra_id?: string;
usuario_id?: string;
status?: string;
data_inicio?: string;
data_fim?: string;
search?: string;
}) => {
return useQuery({
queryKey: ['rdos', 'list', filters || {}],
queryFn: async (): Promise<RDO[]> => {
let query = (supabase as any)
.from('rdos')
.select(`
*,
obra:obras(id, nome, status),
criador:usuarios!rdos_criado_por_fkey(id, nome, email),
aprovador:usuarios!rdos_aprovado_por_fkey(id, nome, email)
`)
.order('data_relatorio', { ascending: false });
if (filters?.obra_id) {
query = query.eq('obra_id', filters.obra_id);
}
if (filters?.usuario_id) {
query = query.eq('criado_por', filters.usuario_id);
}
if (filters?.status) {
query = query.eq('status', filters.status);
}
if (filters?.data_inicio) {
query = query.gte('data_relatorio', filters.data_inicio);
}
if (filters?.data_fim) {
query = query.lte('data_relatorio', filters.data_fim);
}
if (filters?.search) {
query = query.or(`observacoes_gerais.ilike.%${filters.search}%`);
}
const { data, error } = await query;
if (error) {
throw new Error(`Erro ao buscar RDOs: ${error.message}`);
}
return data || [];
},
staleTime: 2 * 60 * 1000, // 2 minutos
});
};
// Hook para buscar RDO por ID
export const useRdo = (id: string | undefined) => {
return useQuery({
queryKey: ['rdos', 'detail', id || ''],
queryFn: async (): Promise<RDO | null> => {
if (!id) return null;
const { data, error } = await (supabase as any)
.from('rdos')
.select(`
*,
obra:obras(id, nome, status, endereco),
criador:usuarios!rdos_criado_por_fkey(id, nome, email, telefone),
aprovador:usuarios!rdos_aprovado_por_fkey(id, nome, email),
atividades:rdo_atividades(
id,
tipo_atividade,
descricao,
percentual_concluido,
ordem
),
mao_obra:rdo_mao_obra(
id,
funcao,
quantidade,
horas_trabalhadas,
observacoes
),
equipamentos:rdo_equipamentos(
id,
nome_equipamento,
tipo,
horas_utilizadas,
combustivel_gasto,
observacoes
),
ocorrencias:rdo_ocorrencias(
id,
tipo_ocorrencia,
descricao,
gravidade,
acao_tomada
)
`)
.eq('id', id)
.single();
if (error) {
if (error.code === 'PGRST116') {
return null; // RDO não encontrado
}
throw new Error(`Erro ao buscar RDO: ${error.message}`);
}
return data;
},
enabled: !!id,
staleTime: 5 * 60 * 1000, // 5 minutos
});
};
// Hook para buscar RDOs de uma obra
export const useObraRdos = (obraId: string | undefined) => {
return useQuery({
queryKey: ['rdos', 'byObra', obraId || ''],
queryFn: async (): Promise<RDO[]> => {
if (!obraId) return [];
const { data, error } = await (supabase as any)
.from('rdos')
.select(`
*,
criador:usuarios!rdos_criado_por_fkey(id, nome, email),
aprovador:usuarios!rdos_aprovado_por_fkey(id, nome, email)
`)
.eq('obra_id', obraId)
.order('data_relatorio', { ascending: false });
if (error) {
throw new Error(`Erro ao buscar RDOs da obra: ${error.message}`);
}
return data || [];
},
enabled: !!obraId,
staleTime: 3 * 60 * 1000, // 3 minutos
});
};
// Hook para buscar RDOs do usuário
export const useUserRdos = (userId: string | undefined) => {
return useQuery({
queryKey: ['rdos', 'byUser', userId || ''],
queryFn: async (): Promise<RDO[]> => {
if (!userId) return [];
const { data, error } = await (supabase as any)
.from('rdos')
.select(`
*,
obra:obras(id, nome, status),
aprovador:usuarios!rdos_aprovado_por_fkey(id, nome, email)
`)
.eq('criado_por', userId)
.order('data_relatorio', { ascending: false });
if (error) {
throw new Error(`Erro ao buscar RDOs do usuário: ${error.message}`);
}
return data || [];
},
enabled: !!userId,
staleTime: 3 * 60 * 1000, // 3 minutos
});
};
// Hook para criar RDO
export const useCreateRdo = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (rdoData: RDOInsert): Promise<RDO> => {
const { data, error } = await (supabase as any)
.from('rdos')
.insert(rdoData)
.select(`
*,
obra:obras(id, nome, status),
criador:usuarios!rdos_criado_por_fkey(id, nome, email)
`)
.single();
if (error) {
throw new Error(`Erro ao criar RDO: ${error.message}`);
}
return data;
},
onSuccess: (newRdo) => {
// Invalidar cache de RDOs
queryClient.invalidateQueries({ queryKey: ['rdos'] });
// Adicionar o novo RDO ao cache
queryClient.setQueryData(
['rdos', 'detail', newRdo.id],
newRdo
);
// Invalidar RDOs da obra
if (newRdo.obra_id) {
queryClient.invalidateQueries({ queryKey: ['rdos', 'byObra', newRdo.obra_id] });
}
// Invalidar RDOs do usuário
if (newRdo.criado_por) {
queryClient.invalidateQueries({
queryKey: ['rdos', 'byUser', newRdo.criado_por]
});
}
},
onError: (error) => {
console.error('Erro ao criar RDO:', error);
},
});
};
// Hook para atualizar RDO
export const useUpdateRdo = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, updates }: { id: string; updates: RDOUpdate }): Promise<RDO> => {
const { data, error } = await (supabase as any)
.from('rdos')
.update(updates)
.eq('id', id)
.select(`
*,
obra:obras(id, nome, status),
criador:usuarios!rdos_criado_por_fkey(id, nome, email),
aprovador:usuarios!rdos_aprovado_por_fkey(id, nome, email)
`)
.single();
if (error) {
throw new Error(`Erro ao atualizar RDO: ${error.message}`);
}
return data;
},
onSuccess: (updatedRdo) => {
// Invalidar cache de RDOs
queryClient.invalidateQueries({ queryKey: ['rdos'] });
// Atualizar o RDO específico no cache
queryClient.setQueryData(
['rdos', 'detail', updatedRdo.id],
updatedRdo
);
// Invalidar RDOs da obra
if (updatedRdo.obra_id) {
queryClient.invalidateQueries({ queryKey: ['rdos', 'byObra', updatedRdo.obra_id] });
}
// Invalidar RDOs do usuário
if (updatedRdo.criado_por) {
queryClient.invalidateQueries({
queryKey: ['rdos', 'byUser', updatedRdo.criado_por]
});
}
},
onError: (error) => {
console.error('Erro ao atualizar RDO:', error);
},
});
};
// Hook para aprovar/rejeitar RDO
export const useApproveRdo = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
id,
status,
aprovadoPor,
observacoesAprovacao
}: {
id: string;
status: 'aprovado' | 'rejeitado';
aprovadoPor: string;
observacoesAprovacao?: string;
}): Promise<RDO> => {
const updates = {
status,
aprovado_por: aprovadoPor,
aprovado_em: new Date().toISOString(),
};
const { data, error } = await (supabase as any)
.from('rdos')
.update(updates)
.eq('id', id)
.select(`
*,
obra:obras(id, nome, status),
criador:usuarios!rdos_criado_por_fkey(id, nome, email),
aprovador:usuarios!rdos_aprovado_por_fkey(id, nome, email)
`)
.single();
if (error) {
throw new Error(`Erro ao ${status === 'aprovado' ? 'aprovar' : 'rejeitar'} RDO: ${error.message}`);
}
return data;
},
onSuccess: (updatedRdo) => {
// Invalidar cache de RDOs
queryClient.invalidateQueries({ queryKey: ['rdos'] });
// Atualizar o RDO específico no cache
queryClient.setQueryData(
['rdos', 'detail', updatedRdo.id],
updatedRdo
);
// Invalidar RDOs da obra
if (updatedRdo.obra_id) {
queryClient.invalidateQueries({ queryKey: ['rdos', 'byObra', updatedRdo.obra_id] });
}
// Invalidar RDOs do usuário
if (updatedRdo.criado_por) {
queryClient.invalidateQueries({
queryKey: ['rdos', 'byUser', updatedRdo.criado_por]
});
}
},
onError: (error) => {
console.error('Erro ao aprovar/rejeitar RDO:', error);
},
});
};
// Hook para deletar RDO
export const useDeleteRdo = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (id: string): Promise<void> => {
const { error } = await supabase
.from('rdos')
.delete()
.eq('id', id);
if (error) {
throw new Error(`Erro ao deletar RDO: ${error.message}`);
}
},
onSuccess: (_, deletedId) => {
// Invalidar cache de RDOs
queryClient.invalidateQueries({ queryKey: ['rdos'] });
// Remover o RDO específico do cache
queryClient.removeQueries({
queryKey: ['rdos', 'detail', deletedId]
});
},
onError: (error) => {
console.error('Erro ao deletar RDO:', error);
},
});
};
// Hook para estatísticas de RDOs
export const useRdosStats = (filters?: {
obra_id?: string;
usuario_id?: string;
data_inicio?: string;
data_fim?: string;
}) => {
return useQuery({
queryKey: ['rdos', 'all', 'stats', filters || {}],
queryFn: async () => {
let query = supabase
.from('rdos')
.select('status, data_relatorio');
if (filters?.obra_id) {
query = query.eq('obra_id', filters.obra_id);
}
if (filters?.usuario_id) {
query = query.eq('criado_por', filters.usuario_id);
}
if (filters?.data_inicio) {
query = query.gte('data_relatorio', filters.data_inicio);
}
if (filters?.data_fim) {
query = query.lte('data_relatorio', filters.data_fim);
}
const { data, error } = await query;
if (error) {
throw new Error(`Erro ao buscar estatísticas: ${error.message}`);
}
const stats = {
total: data?.length || 0,
pendentes: data?.filter((r: any) => r.status === 'pendente').length || 0,
aprovados: data?.filter((r: any) => r.status === 'aprovado').length || 0,
rejeitados: data?.filter((r: any) => r.status === 'rejeitado').length || 0,
por_mes: {} as Record<string, number>,
};
// Agrupar por mês
data?.forEach((rdo: any) => {
const mes = new Date(rdo.data_relatorio).toISOString().substring(0, 7); // YYYY-MM
stats.por_mes[mes] = (stats.por_mes[mes] || 0) + 1;
});
return stats;
},
staleTime: 5 * 60 * 1000, // 5 minutos
});
}

View File

@@ -0,0 +1,242 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '../../lib/supabase';
// import { queryKeys, invalidateQueries } from '../../lib/queryClient';
import type { Usuario, UsuarioInsert, UsuarioUpdate } from '../../types/database.types';
// Type aliases para manter compatibilidade
type User = Usuario;
type UserInsert = UsuarioInsert;
type UserUpdate = UsuarioUpdate;
// Hook para buscar todos os usuários
export const useUsers = (filters?: {
role?: string;
ativo?: boolean;
search?: string;
}) => {
return useQuery({
queryKey: ['users', 'list', filters || {}],
queryFn: async (): Promise<User[]> => {
let query = (supabase as any)
.from('usuarios')
.select('*')
.order('nome');
if (filters?.role) {
query = query.eq('role', filters.role);
}
if (filters?.ativo !== undefined) {
query = query.eq('ativo', filters.ativo);
}
if (filters?.search) {
query = query.or(`nome.ilike.%${filters.search}%,email.ilike.%${filters.search}%`);
}
const { data, error } = await query;
if (error) {
throw new Error(`Erro ao buscar usuários: ${error.message}`);
}
return data || [];
},
staleTime: 5 * 60 * 1000, // 5 minutos
});
};
// Hook para buscar usuário por ID
export const useUser = (id: string | undefined) => {
return useQuery({
queryKey: ['users', 'detail', id || ''],
queryFn: async (): Promise<User | null> => {
if (!id) return null;
const { data, error } = await (supabase as any)
.from('usuarios')
.select('*')
.eq('id', id)
.single();
if (error) {
if (error.code === 'PGRST116') {
return null; // Usuário não encontrado
}
throw new Error(`Erro ao buscar usuário: ${error.message}`);
}
return data;
},
enabled: !!id,
staleTime: 10 * 60 * 1000, // 10 minutos
});
};
// Hook para buscar perfil do usuário atual
export const useUserProfile = () => {
return useQuery({
queryKey: ['users', 'profile'],
queryFn: async (): Promise<User | null> => {
const { data: { user } } = await supabase.auth.getUser();
if (!user) return null;
const { data, error } = await (supabase as any)
.from('usuarios')
.select('*')
.eq('auth_user_id', user.id)
.single();
if (error) {
if (error.code === 'PGRST116') {
return null; // Perfil não encontrado
}
throw new Error(`Erro ao buscar perfil: ${error.message}`);
}
return data;
},
staleTime: 15 * 60 * 1000, // 15 minutos
});
};
// Hook para criar usuário
export const useCreateUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (userData: UserInsert): Promise<User> => {
const { data, error } = await (supabase as any)
.from('usuarios')
.insert(userData)
.select()
.single();
if (error) {
throw new Error(`Erro ao criar usuário: ${error.message}`);
}
return data;
},
onSuccess: (newUser) => {
// Invalidar cache de usuários
queryClient.invalidateQueries({ queryKey: ['users'] });
// Adicionar o novo usuário ao cache
queryClient.setQueryData(
['users', 'detail', newUser.id],
newUser
);
},
onError: (error) => {
console.error('Erro ao criar usuário:', error);
},
});
};
// Hook para atualizar usuário
export const useUpdateUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, updates }: { id: string; updates: UserUpdate }): Promise<User> => {
const { data, error } = await (supabase as any)
.from('usuarios')
.update(updates)
.eq('id', id)
.select()
.single();
if (error) {
throw new Error(`Erro ao atualizar usuário: ${error.message}`);
}
return data;
},
onSuccess: (updatedUser) => {
// Invalidar cache de usuários
queryClient.invalidateQueries({ queryKey: ['users'] });
// Atualizar o usuário específico no cache
queryClient.setQueryData(
['users', 'detail', updatedUser.id],
updatedUser
);
// Se for o perfil atual, atualizar também
const currentProfile = queryClient.getQueryData(['users', 'profile']);
if (currentProfile && (currentProfile as User).id === updatedUser.id) {
queryClient.setQueryData(['users', 'profile'], updatedUser);
}
},
onError: (error) => {
console.error('Erro ao atualizar usuário:', error);
},
});
};
// Hook para deletar usuário (soft delete)
export const useDeleteUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (id: string): Promise<void> => {
const { error } = await (supabase as any)
.from('usuarios')
.update({ ativo: false, deleted_at: new Date().toISOString() })
.eq('id', id);
if (error) {
throw new Error(`Erro ao deletar usuário: ${error.message}`);
}
},
onSuccess: (_, deletedId) => {
// Invalidar cache de usuários
queryClient.invalidateQueries({ queryKey: ['users'] });
// Remover o usuário específico do cache
queryClient.removeQueries({
queryKey: ['users', 'detail', deletedId]
});
},
onError: (error) => {
console.error('Erro ao deletar usuário:', error);
},
});
};
// Hook para reativar usuário
export const useReactivateUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (id: string): Promise<User> => {
const { data, error } = await (supabase as any)
.from('usuarios')
.update({ ativo: true, deleted_at: null })
.eq('id', id)
.select()
.single();
if (error) {
throw new Error(`Erro ao reativar usuário: ${error.message}`);
}
return data;
},
onSuccess: (reactivatedUser) => {
// Invalidar cache de usuários
queryClient.invalidateQueries({ queryKey: ['users'] });
// Atualizar o usuário no cache
queryClient.setQueryData(
['users', 'detail', reactivatedUser.id],
reactivatedUser
);
},
onError: (error) => {
console.error('Erro ao reativar usuário:', error);
},
});
};

438
src/hooks/useAuth.ts Normal file
View File

@@ -0,0 +1,438 @@
import { useState, useEffect } from 'react';
import { User, Session, AuthError } from '@supabase/supabase-js';
import { supabase } from '../lib/supabase';
interface AuthState {
user: User | null;
session: Session | null;
loading: boolean;
error: string | null;
}
interface LoginCredentials {
email: string;
password: string;
}
interface RegisterCredentials extends LoginCredentials {
nome: string;
cpf?: string;
telefone?: string;
}
export const useAuth = () => {
const [authState, setAuthState] = useState<AuthState>({
user: null,
session: null,
loading: true,
error: null
});
useEffect(() => {
// Verificar sessão atual
const getSession = async () => {
try {
const { data: { session }, error } = await supabase.auth.getSession();
if (error) throw error;
console.log('✅ useAuth: Sessão recuperada:', session?.user?.email);
// Se não tiver usuário, finaliza loading imediatamente
if (!session?.user) {
setAuthState({
user: null,
session: null,
loading: false,
error: null
});
console.log('⚠️ useAuth: Nenhuma sessão ativa');
return;
}
// Se tiver usuário, mantém loading true enquanto busca perfil
setAuthState(prev => ({
...prev,
user: session.user,
session,
loading: true, // Mantém carregando
error: null
}));
// CRÍTICO: Carregar dados do perfil (role, organização) no refresh
console.log('🔄 useAuth: Recuperando perfil do usuário após refresh...', session.user.id);
try {
// Garantir que o perfil existe antes de buscar
await syncUserProfile(session.user);
const { useUserStore } = await import('../stores/useUserStore');
const profilePromise = useUserStore.getState().fetchCurrentUser(session.user.id);
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout fetchCurrentUser')), 15000));
await Promise.race([profilePromise, timeoutPromise]);
console.log('✅ useAuth: Perfil carregado com sucesso');
} catch (err) {
console.error('❌ useAuth: Erro/Timeout ao carregar perfil:', err);
} finally {
// Só agora libera o loading
setAuthState(prev => ({ ...prev, loading: false }));
}
} catch (error: unknown) {
let errorMessage = 'Erro desconhecido';
if (error instanceof Error) errorMessage = error.message;
setAuthState({
user: null,
session: null,
loading: false,
error: errorMessage
});
}
};
getSession();
// Escutar mudanças de autenticação
const { data: { subscription } } = supabase.auth.onAuthStateChange(
async (event, session) => {
console.log('🔔 Auth state changed:', event, session?.user?.email);
// Se estiver fazendo login, não tira o loading ainda, deixa o fluxo de login/getSession lidar
// Mas se for atualização de token ou signout, atualiza
if (event === 'SIGNED_OUT') {
setAuthState({
user: null,
session: null,
loading: false,
error: null
});
return;
}
// Se o usuário fez login (ou token refresh), sincronizar
if (session?.user) {
// Atualiza sessão mas mantém loading se for login inicial (tratado pelo getSession ou login)
// Para eventos intermediários (TOKEN_REFRESHED), apenas atualiza sessão
setAuthState(prev => ({
...prev,
user: session.user,
session,
// Não forçamos loading false aqui para não sobrescrever operações em andamento
}));
if (event === 'SIGNED_IN' || event === 'TOKEN_REFRESHED') {
console.log('🔄 Sincronizando perfil (Auth Change)...');
try {
// Sync já tem timeout interno
await syncUserProfile(session.user);
// Garantir que temos os dados no store (com timeout)
const { useUserStore } = await import('../stores/useUserStore');
const currentUser = useUserStore.getState().currentUser;
if (!currentUser || currentUser.id !== session.user.id) {
const profilePromise = useUserStore.getState().fetchCurrentUser(session.user.id);
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout fetchCurrentUser AuthChange')), 15000));
await Promise.race([profilePromise, timeoutPromise]);
}
} catch (err) {
console.error('Erro/Timeout ao sincronizar perfil no AuthChange:', err);
} finally {
// Finaliza loading após garantir dados
setAuthState(prev => ({ ...prev, loading: false }));
}
} else {
// Para outros eventos onde temos user mas não é login/refresh (ex: USER_UPDATED),
// garantimos que loading não fique preso se estava true
setAuthState(prev => ({ ...prev, loading: false }));
}
}
}
);
return () => subscription.unsubscribe();
}, []);
const syncUserProfile = async (user: User) => {
try {
console.log('🔄 syncUserProfile: Sincronizando dados:', user.email);
// Wrapper de timeout para operações de banco
const dbTimeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout syncUserProfile')), 15000));
// Verificar se o usuário existe na tabela usuarios
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const fetchPromise = (supabase as any)
.from('usuarios')
.select('*')
.eq('email', user.email)
.single();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { data: existingUser, error: fetchError } = await Promise.race([fetchPromise, dbTimeout]) as any;
if (fetchError && fetchError.code !== 'PGRST116') {
console.error('Erro ao buscar usuário (Sync):', fetchError);
return;
}
// Se não existe, criar registro na tabela usuarios
if (!existingUser) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const insertPromise = (supabase as any)
.from('usuarios')
.insert({
id: user.id,
email: user.email!,
nome: user.user_metadata?.full_name || user.user_metadata?.nome || user.email?.split('@')[0] || 'Usuário',
ativo: true
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { error: insertError } = await Promise.race([insertPromise, dbTimeout]) as any;
if (insertError) {
console.error('Erro ao criar perfil do usuário:', insertError);
}
}
} catch (error) {
console.error('Erro/Timeout na sincronização do perfil:', error);
}
};
const login = async (credentials: LoginCredentials) => {
try {
console.log('🔐 useAuth: Iniciando login...');
console.log('📧 useAuth: Email:', credentials.email);
setAuthState(prev => ({ ...prev, loading: true, error: null }));
console.log('🌐 useAuth: Chamando supabase.auth.signInWithPassword...');
const { data, error } = await supabase.auth.signInWithPassword({
email: credentials.email,
password: credentials.password
});
console.log('📊 useAuth: Resposta do Supabase:', { data: !!data, error: error?.message });
if (error) throw error;
console.log('✅ useAuth: Login bem-sucedido');
// Carregar perfil completo do usuário (com organizacao_id) no store global
if (data.user) {
// Importação dinâmica para evitar ciclos se necessário, ou assumir import no topo
const { useUserStore } = await import('../stores/useUserStore');
await useUserStore.getState().fetchCurrentUser(data.user.id);
}
return { success: true, data };
} catch (error: unknown) {
console.log('❌ useAuth: Erro no login:', error instanceof Error ? error.message : String(error));
const errorMessage = getAuthErrorMessage(error as AuthError);
setAuthState(prev => ({ ...prev, loading: false, error: errorMessage }));
return { success: false, error: errorMessage };
}
};
const register = async (credentials: RegisterCredentials) => {
try {
setAuthState(prev => ({ ...prev, loading: true, error: null }));
const { data, error } = await supabase.auth.signUp({
email: credentials.email,
password: credentials.password,
options: {
data: {
nome: credentials.nome,
cpf: credentials.cpf,
telefone: credentials.telefone
}
}
});
if (error) throw error;
return { success: true, data };
} catch (error: unknown) {
const errorMessage = getAuthErrorMessage(error as AuthError);
setAuthState(prev => ({ ...prev, loading: false, error: errorMessage }));
return { success: false, error: errorMessage };
}
};
const logout = async () => {
console.log('🚪 useAuth: Iniciando logout imediato...');
// 1. Limpar tokens do localStorage (exceto preferências de tema talvez, mas por segurança limpamos chaves auth)
Object.keys(localStorage).forEach(key => {
if (key.includes('sb-') || key.includes('supabase') || key.includes('auth')) {
localStorage.removeItem(key);
}
});
// 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));
// 3. Limpar estado local do hook
setAuthState({
user: null,
session: null,
loading: false,
error: null
});
// 4. Redirecionar forçadamente para /login
// Usamos replace para não permitir voltar
window.location.replace('/login');
return { success: true };
};
const resetPassword = async (email: string) => {
try {
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${window.location.origin}/reset-password`
});
if (error) throw error;
return { success: true };
} catch (error: unknown) {
return { success: false, error: getAuthErrorMessage(error as AuthError) };
}
};
const updatePassword = async (newPassword: string) => {
try {
const { error } = await supabase.auth.updateUser({
password: newPassword
});
if (error) throw error;
return { success: true };
} catch (error: unknown) {
return { success: false, error: getAuthErrorMessage(error as AuthError) };
}
};
const updateProfile = async (updates: Partial<{ nome: string; cpf: string; telefone: string }>) => {
try {
if (!authState.user) throw new Error('Usuário não autenticado');
// Atualizar metadados do usuário
const { error: authError } = await supabase.auth.updateUser({
data: updates
});
if (authError) throw authError;
// Atualizar tabela usuarios
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { error: dbError } = await (supabase as any)
.from('usuarios')
.update(updates)
.eq('id', authState.user.id);
if (dbError) throw dbError;
return { success: true };
} catch (error: unknown) {
return { success: false, error: getAuthErrorMessage(error as AuthError) };
}
};
const clearError = () => {
setAuthState(prev => ({ ...prev, error: null }));
};
// Função de bypass para desenvolvimento
const bypassLogin = async () => {
console.log('🚧 useAuth: Iniciando bypass de desenvolvimento...');
try {
setAuthState(prev => ({ ...prev, loading: true, error: null }));
// Simular um usuário autenticado
const mockUser = {
id: 'bypass-user-' + Date.now(),
email: 'bypass@desenvolvimento.com',
user_metadata: {
nome: 'Usuário Bypass'
},
aud: 'authenticated',
role: 'authenticated',
app_metadata: {},
created_at: new Date().toISOString()
};
const mockSession = {
access_token: 'mock-token',
refresh_token: 'mock-refresh',
expires_in: 3600,
token_type: 'bearer',
user: mockUser
};
// Atualizar estado de autenticação
setAuthState({
user: mockUser as unknown as User,
session: mockSession as unknown as Session,
loading: false,
error: null
});
console.log('✅ useAuth: Bypass concluído com sucesso');
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);
setAuthState(prev => ({ ...prev, loading: false, error: 'Erro no bypass' }));
return { success: false, error: 'Erro no bypass' };
}
};
return {
// Estado
user: authState.user,
session: authState.session,
loading: authState.loading,
error: authState.error,
isAuthenticated: !!authState.user,
// Ações
login,
register,
logout,
resetPassword,
updatePassword,
updateProfile,
clearError,
bypassLogin
};
};
// Função auxiliar para traduzir erros de autenticação
const getAuthErrorMessage = (error: AuthError | Error): string => {
if ('message' in error) {
switch (error.message) {
case 'Invalid login credentials':
return 'Credenciais de login inválidas';
case 'Email not confirmed':
return 'Email não confirmado. Verifique sua caixa de entrada';
case 'User already registered':
return 'Usuário já cadastrado com este email';
case 'Password should be at least 6 characters':
return 'A senha deve ter pelo menos 6 caracteres';
case 'Unable to validate email address: invalid format':
return 'Formato de email inválido';
case 'Email rate limit exceeded':
return 'Limite de emails excedido. Tente novamente mais tarde';
default:
return error.message;
}
}
return 'Erro desconhecido';
};
export default useAuth;

363
src/hooks/useInviteCode.ts Normal file
View File

@@ -0,0 +1,363 @@
import { useState, useCallback } from 'react';
import { supabase } from '../lib/supabase';
interface ConviteResult {
success: boolean;
organizacao_id?: string;
organizacao_nome?: string;
role?: string;
error?: string;
}
interface UseInviteCodeReturn {
loading: boolean;
error: string | null;
validarConvite: (codigo: string) => Promise<ConviteResult>;
usarConvite: (codigo: string, usuarioId: string) => Promise<ConviteResult>;
gerarConvite: (organizacaoId: string, options?: GerarConviteOptions) => Promise<{ success: boolean; codigo?: string; error?: string }>;
listarConvites: (organizacaoId: string) => Promise<ConviteRow[]>;
}
interface GerarConviteOptions {
emailConvidado?: string;
role?: string;
maxUsos?: number;
expiraEmDias?: number;
criadoPor?: string;
}
// Tipo local para convite (tabela não está no database.types.ts gerado)
interface ConviteRow {
id: string;
organizacao_id: string;
codigo: string;
criado_por: string | null;
email_convidado: string | null;
role: string;
max_usos: number;
usos_atuais: number;
ativo: boolean;
expira_em: string | null;
created_at: string;
updated_at: string;
organizacoes?: { nome: string } | null;
}
export const useInviteCode = (): UseInviteCodeReturn => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
/**
* Valida se um código de convite existe e está ativo
*/
const validarConvite = useCallback(async (codigo: string): Promise<ConviteResult> => {
try {
setLoading(true);
setError(null);
const codigoFormatado = codigo.toUpperCase().trim();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { data, error: queryError } = await (supabase as any)
.from('convites')
.select(`
id,
codigo,
organizacao_id,
role,
max_usos,
usos_atuais,
ativo,
expira_em,
email_convidado,
organizacoes:organizacao_id (nome)
`)
.eq('codigo', codigoFormatado)
.eq('ativo', true)
.single();
console.log('useInviteCode: validando código:', codigoFormatado);
console.log('useInviteCode: query result:', { data, queryError });
if (queryError || !data) {
console.error('useInviteCode: erro ou sem dados:', queryError);
return {
success: false,
error: 'Código de convite inválido ou expirado.'
};
}
const convite = data as ConviteRow;
// Verificar expiração
if (convite.expira_em && new Date(convite.expira_em) < new Date()) {
return {
success: false,
error: 'Este código de convite expirou.'
};
}
// Verificar limite de usos
if (convite.max_usos > 0 && convite.usos_atuais >= convite.max_usos) {
return {
success: false,
error: 'Este código de convite já atingiu o limite de usos.'
};
}
return {
success: true,
organizacao_id: convite.organizacao_id,
organizacao_nome: convite.organizacoes?.nome || 'Organização',
role: convite.role
};
} catch (err) {
const msg = err instanceof Error ? err.message : 'Erro ao validar convite';
setError(msg);
return { success: false, error: msg };
} finally {
setLoading(false);
}
}, []);
/**
* Usa o código de convite para associar o usuário à organização
*/
const usarConvite = useCallback(async (codigo: string, usuarioId: string): Promise<ConviteResult> => {
try {
setLoading(true);
setError(null);
console.log('useInviteCode: chamando usar_convite RPC:', { codigo, usuarioId });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { data, error: rpcError } = await (supabase as any).rpc('usar_convite', {
p_codigo: codigo,
p_usuario_id: usuarioId
});
console.log('useInviteCode: resultado RPC:', { data, rpcError });
if (rpcError) {
console.error('useInviteCode: erro RPC:', rpcError);
throw rpcError;
}
const result = data as ConviteResult;
if (!result.success) {
setError(result.error || 'Erro ao usar convite');
}
return result;
} catch (err) {
const msg = err instanceof Error ? err.message : 'Erro ao usar convite';
setError(msg);
return { success: false, error: msg };
} finally {
setLoading(false);
}
}, []);
/**
* Gera um novo código de convite (apenas admins)
*/
const gerarConvite = useCallback(async (
organizacaoId: string,
options: GerarConviteOptions = {}
) => {
try {
setLoading(true);
setError(null);
console.log('useInviteCode: Iniciando geração de convite para org:', organizacaoId, options);
// Gerar código aleatório LOCAL (fallback para evitar erro de RPC)
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const nums = '0123456789';
let codigo = '';
for (let i = 0; i < 3; i++) codigo += chars.charAt(Math.floor(Math.random() * chars.length));
for (let i = 0; i < 4; i++) codigo += nums.charAt(Math.floor(Math.random() * nums.length));
console.log('useInviteCode: Código gerado localmente:', codigo);
// Calcular data de expiração
let expiraEm: string | null = null;
if (options.expiraEmDias) {
const date = new Date();
date.setDate(date.getDate() + options.expiraEmDias);
expiraEm = date.toISOString();
}
// Determinar ID do criador
let userId = options.criadoPor;
if (!userId) {
console.warn('useInviteCode: ID do criador não fornecido. Tentando obter via Supabase Auth...');
try {
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject('Timeout'), 2000));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { data } = await Promise.race([supabase.auth.getUser(), timeoutPromise]) as any;
if (data?.user?.id) {
userId = data.user.id;
console.log('useInviteCode: Usuário recuperado via Auth:', userId);
}
} catch (e) {
console.warn('useInviteCode: Timeout/Erro ao obter user via Auth:', e);
}
// Fallback: LocalStorage
if (!userId) {
console.warn('useInviteCode: Tentando recuperar usuário do LocalStorage...');
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key?.startsWith('sb-') && key?.endsWith('-auth-token')) {
const val = localStorage.getItem(key);
if (val) {
const parsed = JSON.parse(val);
if (parsed.user?.id) {
userId = parsed.user.id;
console.log('useInviteCode: Usuário recuperado via LocalStorage:', userId);
break;
}
}
}
}
}
}
if (!userId) {
throw new Error('Não foi possível identificar o usuário para criar o convite.');
}
// Obter email para log/debug se possível (opcional)
console.log('useInviteCode: Inserindo convite no banco...', {
organizacao_id: organizacaoId,
codigo,
criado_por: userId
});
// TENTATIVA 1: RAW FETCH (Bypass Client)
// Para evitar hangs do client websocket/session
// 1. Recuperar Token (COM TIMEOUT PARA EVITAR HANG)
let token = null;
try {
// Tenta pegar da sessão atual com timeout curto (1.5s)
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject('Timeout'), 1500));
const sessionPromise = supabase.auth.getSession();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { data: sessionData } = await Promise.race([sessionPromise, timeoutPromise]) as any;
if (sessionData?.session?.access_token) {
token = sessionData.session.access_token;
// console.log('useInviteCode: Token obtido via Session');
}
} catch {
console.warn('useInviteCode: Timeout/Erro ao pegar sessão (bypass para LocalStorage)');
}
if (!token) {
// Fallback: localStorage
console.log('useInviteCode: Buscando token no LocalStorage...');
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key?.startsWith('sb-') && key?.endsWith('-auth-token')) {
const val = localStorage.getItem(key);
if (val) {
const parsed = JSON.parse(val);
if (parsed.access_token) {
token = parsed.access_token;
console.log('useInviteCode: Token obtido via LocalStorage');
break;
}
}
}
}
}
if (!token) {
throw new Error('Não foi possível obter o token de autenticação para a requisição.');
}
// 2. Montar objeto
const novoConvite = {
organizacao_id: organizacaoId,
codigo,
criado_por: userId,
email_convidado: options.emailConvidado || null,
role: options.role || 'usuario',
max_usos: options.maxUsos ?? 1,
expira_em: expiraEm,
};
// 3. Executar fetch
const baseUrl = import.meta.env.VITE_SUPABASE_URL;
const anonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
console.log('useInviteCode: Enviando RAW FETCH insert...');
const res = await fetch(`${baseUrl}/rest/v1/convites`, {
method: 'POST',
headers: {
'apikey': anonKey,
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'Prefer': 'return=minimal' // Não precisamos do retorno
},
body: JSON.stringify(novoConvite)
});
if (!res.ok) {
const text = await res.text();
console.error('useInviteCode: Erro RAW FETCH:', text);
throw new Error(`Erro ao salvar convite: ${res.statusText}`);
}
console.log('useInviteCode: Convite gerado com sucesso (RAW FETCH)!');
return { success: true, codigo };
} catch (err) {
console.error('useInviteCode: Catch error in gerarConvite:', err);
const msg = err instanceof Error ? err.message : 'Erro ao gerar convite';
setError(msg);
return { success: false, error: msg };
} finally {
setLoading(false);
}
}, []);
/**
* Lista convites de uma organização
*/
const listarConvites = useCallback(async (organizacaoId: string) => {
try {
setLoading(true);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { data, error: queryError } = await (supabase as any)
.from('convites')
.select('*')
.eq('organizacao_id', organizacaoId)
.order('created_at', { ascending: false });
if (queryError) throw queryError;
return data || [];
} catch (err) {
console.error('Erro ao listar convites:', err);
return [];
} finally {
setLoading(false);
}
}, []);
return {
loading,
error,
validarConvite,
usarConvite,
gerarConvite,
listarConvites,
};
};

163
src/hooks/useMFA.ts Normal file
View File

@@ -0,0 +1,163 @@
/**
* Hook para gerenciar MFA
*/
import { useState, useCallback } from 'react';
import { MFAService, type MFAEnrollment, type BackupCode } from '../services/mfaService';
export const useMFA = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [enrollment, setEnrollment] = useState<MFAEnrollment | null>(null);
const [backupCodes, setBackupCodes] = useState<BackupCode[]>([]);
/**
* Inicia enrollment do MFA
*/
const startEnrollment = useCallback(async (friendlyName?: string) => {
setLoading(true);
setError(null);
const { data, error: enrollError } = await MFAService.enroll(friendlyName);
if (enrollError) {
setError(enrollError);
setLoading(false);
return { success: false, error: enrollError };
}
setEnrollment(data);
setLoading(false);
return { success: true, data };
}, []);
/**
* Verifica código e completa enrollment
*/
const verifyEnrollment = useCallback(async (code: string) => {
if (!enrollment) {
setError('Nenhum enrollment ativo');
return { success: false, error: 'Nenhum enrollment ativo' };
}
setLoading(true);
setError(null);
const { error: verifyError } = await MFAService.verify(enrollment.id, code);
if (verifyError) {
setError(verifyError);
setLoading(false);
return { success: false, error: verifyError };
}
// Gerar códigos de backup
const codes = MFAService.generateBackupCodes();
setBackupCodes(codes);
setLoading(false);
return { success: true, backupCodes: codes };
}, [enrollment]);
/**
* Desativa MFA
*/
const disableMFA = useCallback(async (factorId: string) => {
setLoading(true);
setError(null);
const { error: unenrollError } = await MFAService.unenroll(factorId);
if (unenrollError) {
setError(unenrollError);
setLoading(false);
return { success: false, error: unenrollError };
}
setEnrollment(null);
setBackupCodes([]);
setLoading(false);
return { success: true };
}, []);
/**
* Verifica código durante login
*/
const verifyCode = useCallback(async (factorId: string, code: string) => {
setLoading(true);
setError(null);
// Criar challenge
const { challengeId, error: challengeError } = await MFAService.challenge(factorId);
if (challengeError || !challengeId) {
setError(challengeError || 'Erro ao criar challenge');
setLoading(false);
return { success: false, error: challengeError };
}
// Verificar código
const { error: verifyError } = await MFAService.verifyChallenge(
factorId,
challengeId,
code
);
if (verifyError) {
setError(verifyError);
setLoading(false);
return { success: false, error: verifyError };
}
setLoading(false);
return { success: true };
}, []);
/**
* Lista fatores MFA
*/
const listFactors = useCallback(async () => {
setLoading(true);
setError(null);
const { factors, error: listError } = await MFAService.listFactors();
if (listError) {
setError(listError);
setLoading(false);
return { factors: [], error: listError };
}
setLoading(false);
return { factors, error: null };
}, []);
/**
* Verifica se MFA está ativo
*/
const checkMFAStatus = useCallback(async () => {
const hasMFA = await MFAService.hasMFA();
return hasMFA;
}, []);
/**
* Limpa erro
*/
const clearError = useCallback(() => {
setError(null);
}, []);
return {
loading,
error,
enrollment,
backupCodes,
startEnrollment,
verifyEnrollment,
disableMFA,
verifyCode,
listFactors,
checkMFAStatus,
clearError,
};
};

474
src/hooks/useOffline.ts Normal file
View File

@@ -0,0 +1,474 @@
import { useEffect, useState, useCallback } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { OfflineManager, offlineDb } from '../lib/offlineDb';
import { supabase } from '../lib/supabase';
import { queryKeys, invalidateQueries } from '../lib/queryClient';
import type { Usuario, Obra, RDO } from '../types/database.types';
import type { PendingOperation } from '../lib/offlineDb';
// Hook principal para funcionalidades offline
export const useOffline = () => {
const [isOnline, setIsOnline] = useState(navigator.onLine);
const [isSyncing, setIsSyncing] = useState(false);
const [pendingOperations, setPendingOperations] = useState<PendingOperation[]>([]);
const queryClient = useQueryClient();
// Monitorar status de conectividade
useEffect(() => {
const handleOnline = () => {
setIsOnline(true);
console.log('Conexão restaurada - iniciando sincronização');
syncPendingOperations();
};
const handleOffline = () => {
setIsOnline(false);
console.log('Conexão perdida - modo offline ativado');
};
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
// Carregar operações pendentes
const loadPendingOperations = useCallback(async () => {
const operations = await OfflineManager.getPendingOperations();
setPendingOperations(operations);
}, []);
// Sincronizar operações pendentes
const syncPendingOperations = useCallback(async () => {
if (!isOnline || isSyncing) return;
setIsSyncing(true);
try {
const operations = await OfflineManager.getPendingOperations();
for (const operation of operations) {
try {
await processPendingOperation(operation);
await OfflineManager.removePendingOperation(operation.id!);
} catch (error) {
console.error('Error processing pending operation:', error);
await OfflineManager.markOperationError(
operation.id!,
error instanceof Error ? error.message : 'Unknown error'
);
}
}
// Recarregar operações pendentes
await loadPendingOperations();
// Invalidar queries para atualizar dados
invalidateQueries.all();
console.log('Sincronização concluída');
} catch (error) {
console.error('Error syncing pending operations:', error);
} finally {
setIsSyncing(false);
}
}, [isOnline, isSyncing]);
// Processar uma operação pendente
const processPendingOperation = async (operation: PendingOperation) => {
const { table, operation: op, data } = operation;
switch (table) {
case 'usuarios':
if (op === 'create') {
await (supabase as any).from('usuarios').insert(data);
} else if (op === 'update') {
const { id, ...updateData } = data;
await (supabase as any).from('usuarios').update(updateData).eq('id', id);
} else if (op === 'delete') {
await supabase.from('usuarios').delete().eq('id', data.id);
}
break;
case 'obras':
if (op === 'create') {
await (supabase as any).from('obras').insert(data);
} else if (op === 'update') {
const { id, ...updateData } = data;
await (supabase as any).from('obras').update(updateData).eq('id', id);
} else if (op === 'delete') {
await supabase.from('obras').delete().eq('id', data.id);
}
break;
case 'rdos':
if (op === 'create') {
await (supabase as any).from('rdos').insert(data);
} else if (op === 'update') {
const { id, ...updateData } = data;
await (supabase as any).from('rdos').update(updateData).eq('id', id);
} else if (op === 'delete') {
await supabase.from('rdos').delete().eq('id', data.id);
}
break;
}
};
// Cache dados para uso offline
const cacheDataForOffline = useCallback(async () => {
if (!isOnline) return;
try {
// Cache usuários
const { data: usuarios } = await supabase
.from('usuarios')
.select('*')
.eq('ativo', true);
if (usuarios) {
await OfflineManager.cacheData('usuarios', usuarios);
}
// Cache obras
const { data: obras } = await supabase
.from('obras')
.select('*');
if (obras) {
await OfflineManager.cacheData('obras', obras);
}
// Cache RDOs (últimos 30 dias)
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const { data: rdos } = await supabase
.from('rdos')
.select('*')
.gte('created_at', thirtyDaysAgo.toISOString());
if (rdos) {
await OfflineManager.cacheData('rdos', rdos);
}
// Salvar timestamp da última sincronização
await OfflineManager.setConfig('lastFullSync', Date.now());
console.log('Dados cacheados para uso offline');
} catch (error) {
console.error('Error caching data for offline:', error);
}
}, [isOnline]);
// Inicializar na montagem
useEffect(() => {
loadPendingOperations();
if (isOnline) {
cacheDataForOffline();
}
}, [loadPendingOperations, cacheDataForOffline, isOnline]);
return {
isOnline,
isSyncing,
pendingOperations,
syncPendingOperations,
cacheDataForOffline,
loadPendingOperations,
};
};
// Hook para operações offline de usuários
export const useOfflineUsers = () => {
const { isOnline } = useOffline();
const queryClient = useQueryClient();
const createUserOffline = useCallback(async (userData: Omit<Usuario, 'id' | 'created_at' | 'updated_at'>) => {
const tempId = `temp_${Date.now()}`;
const newUser = {
...userData,
id: tempId,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
// Salvar no cache local
await offlineDb.usuarios.add({
...newUser,
_pendingSync: true,
_lastSync: Date.now(),
});
// Adicionar operação pendente
await OfflineManager.addPendingOperation('usuarios', 'create', newUser);
// Invalidar queries para atualizar UI
queryClient.invalidateQueries({ queryKey: queryKeys.users.all });
return newUser;
}, [queryClient]);
const updateUserOffline = useCallback(async (id: string, userData: Partial<Usuario>) => {
const updatedData = {
...userData,
id,
updated_at: new Date().toISOString(),
};
// Atualizar no cache local
await offlineDb.usuarios.update(id, {
...updatedData,
_pendingSync: true,
_lastSync: Date.now(),
});
// Adicionar operação pendente
await OfflineManager.addPendingOperation('usuarios', 'update', updatedData);
// Invalidar queries para atualizar UI
queryClient.invalidateQueries({ queryKey: queryKeys.users.detail(id) });
queryClient.invalidateQueries({ queryKey: queryKeys.users.all });
return updatedData;
}, [queryClient]);
const deleteUserOffline = useCallback(async (id: string) => {
// Marcar como deletado no cache local
await offlineDb.usuarios.update(id, {
_deleted: true,
_pendingSync: true,
_lastSync: Date.now(),
});
// Adicionar operação pendente
await OfflineManager.addPendingOperation('usuarios', 'delete', { id });
// Invalidar queries para atualizar UI
queryClient.invalidateQueries({ queryKey: queryKeys.users.detail(id) });
queryClient.invalidateQueries({ queryKey: queryKeys.users.all });
}, [queryClient]);
const getUsersOffline = useCallback(async (): Promise<Usuario[]> => {
return await OfflineManager.getCachedData<Usuario>('usuarios');
}, []);
return {
createUserOffline,
updateUserOffline,
deleteUserOffline,
getUsersOffline,
isOnline,
};
};
// Hook para operações offline de obras
export const useOfflineObras = () => {
const { isOnline } = useOffline();
const queryClient = useQueryClient();
const createObraOffline = useCallback(async (obraData: Omit<Obra, 'id' | 'created_at' | 'updated_at'>) => {
const tempId = `temp_${Date.now()}`;
const newObra = {
...obraData,
id: tempId,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
// Salvar no cache local
await offlineDb.obras.add({
...newObra,
_pendingSync: true,
_lastSync: Date.now(),
});
// Adicionar operação pendente
await OfflineManager.addPendingOperation('obras', 'create', newObra);
// Invalidar queries para atualizar UI
queryClient.invalidateQueries({ queryKey: queryKeys.obras.all });
return newObra;
}, [queryClient]);
const updateObraOffline = useCallback(async (id: string, obraData: Partial<Obra>) => {
const updatedData = {
...obraData,
id,
updated_at: new Date().toISOString(),
};
// Atualizar no cache local
await offlineDb.obras.update(id, {
...updatedData,
_pendingSync: true,
_lastSync: Date.now(),
});
// Adicionar operação pendente
await OfflineManager.addPendingOperation('obras', 'update', updatedData);
// Invalidar queries para atualizar UI
queryClient.invalidateQueries({ queryKey: queryKeys.obras.detail(id) });
queryClient.invalidateQueries({ queryKey: queryKeys.obras.all });
return updatedData;
}, [queryClient]);
const deleteObraOffline = useCallback(async (id: string) => {
// Marcar como deletado no cache local
await offlineDb.obras.update(id, {
_deleted: true,
_pendingSync: true,
_lastSync: Date.now(),
});
// Adicionar operação pendente
await OfflineManager.addPendingOperation('obras', 'delete', { id });
// Invalidar queries para atualizar UI
queryClient.invalidateQueries({ queryKey: queryKeys.obras.detail(id) });
queryClient.invalidateQueries({ queryKey: queryKeys.obras.all });
}, [queryClient]);
const getObrasOffline = useCallback(async (): Promise<Obra[]> => {
return await OfflineManager.getCachedData<Obra>('obras');
}, []);
return {
createObraOffline,
updateObraOffline,
deleteObraOffline,
getObrasOffline,
isOnline,
};
};
// Hook para operações offline de RDOs
export const useOfflineRdos = () => {
const { isOnline } = useOffline();
const queryClient = useQueryClient();
const createRdoOffline = useCallback(async (rdoData: Omit<RDO, 'id' | 'created_at' | 'updated_at'>) => {
const tempId = `temp_${Date.now()}`;
const newRdo = {
...rdoData,
id: tempId,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
// Salvar no cache local
await offlineDb.rdos.add({
...newRdo,
_pendingSync: true,
_lastSync: Date.now(),
});
// Adicionar operação pendente
await OfflineManager.addPendingOperation('rdos', 'create', newRdo);
// Invalidar queries para atualizar UI
queryClient.invalidateQueries({ queryKey: queryKeys.rdos.all });
if (rdoData.obra_id) {
queryClient.invalidateQueries({ queryKey: queryKeys.rdos.byObra(rdoData.obra_id) });
}
return newRdo;
}, [queryClient]);
const updateRdoOffline = useCallback(async (id: string, rdoData: Partial<RDO>) => {
const updatedData = {
...rdoData,
id,
updated_at: new Date().toISOString(),
};
// Atualizar no cache local
await offlineDb.rdos.update(id, {
...updatedData,
_pendingSync: true,
_lastSync: Date.now(),
});
// Adicionar operação pendente
await OfflineManager.addPendingOperation('rdos', 'update', updatedData);
// Invalidar queries para atualizar UI
queryClient.invalidateQueries({ queryKey: queryKeys.rdos.detail(id) });
queryClient.invalidateQueries({ queryKey: queryKeys.rdos.all });
if (rdoData.obra_id) {
queryClient.invalidateQueries({ queryKey: queryKeys.rdos.byObra(rdoData.obra_id) });
}
return updatedData;
}, [queryClient]);
const deleteRdoOffline = useCallback(async (id: string) => {
// Marcar como deletado no cache local
await offlineDb.rdos.update(id, {
_deleted: true,
_pendingSync: true,
_lastSync: Date.now(),
});
// Adicionar operação pendente
await OfflineManager.addPendingOperation('rdos', 'delete', { id });
// Invalidar queries para atualizar UI
queryClient.invalidateQueries({ queryKey: queryKeys.rdos.detail(id) });
queryClient.invalidateQueries({ queryKey: queryKeys.rdos.all });
}, [queryClient]);
const getRdosOffline = useCallback(async (obraId?: string): Promise<RDO[]> => {
if (obraId) {
return await OfflineManager.getCachedData<RDO>('rdos', (rdo: RDO) => rdo.obra_id === obraId);
}
return await OfflineManager.getCachedData<RDO>('rdos');
}, []);
return {
createRdoOffline,
updateRdoOffline,
deleteRdoOffline,
getRdosOffline,
isOnline,
};
};
// Hook para estatísticas offline
export const useOfflineStats = () => {
const [stats, setStats] = useState<{
usuarios: number;
obras: number;
rdos: number;
pendingOperations: number;
lastSync?: number;
}>({
usuarios: 0,
obras: 0,
rdos: 0,
pendingOperations: 0,
});
const loadStats = useCallback(async () => {
const cacheStats = await OfflineManager.getCacheStats();
setStats(cacheStats);
}, []);
useEffect(() => {
loadStats();
// Atualizar estatísticas a cada 30 segundos
const interval = setInterval(loadStats, 30000);
return () => clearInterval(interval);
}, [loadStats]);
return {
stats,
loadStats,
};
};

82
src/hooks/useRDO.ts Normal file
View File

@@ -0,0 +1,82 @@
import { useState, useEffect, useCallback } from 'react';
import { supabase } from '../lib/supabase';
import type {
RDOCompleto
} from '../types/database.types';
export const useRDO = (rdoId: string | undefined) => {
const [rdo, setRdo] = useState<RDOCompleto | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchRDO = useCallback(async () => {
if (!rdoId) return;
try {
setLoading(true);
setError(null);
// 1. Buscar dados principais do RDO
const { data: rdoData, error: rdoError } = await supabase
.from('rdos')
.select(`
*,
obra:obras(*),
criador:usuarios!rdos_criado_por_fkey(*)
`)
.eq('id', rdoId)
.single();
if (rdoError) throw rdoError;
if (!rdoData) throw new Error('RDO não encontrado');
// 2. Buscar dados relacionados em paralelo
const [
{ data: atividades },
{ data: maoDeObra },
{ data: equipamentos },
{ data: ocorrencias },
{ data: anexos },
{ data: inspecoesSolda },
{ data: verificacoesTorque }
] = await Promise.all([
supabase.from('rdo_atividades').select('*').eq('rdo_id', rdoId).order('ordem'),
supabase.from('rdo_mao_obra').select('*').eq('rdo_id', rdoId),
supabase.from('rdo_equipamentos').select('*').eq('rdo_id', rdoId),
supabase.from('rdo_ocorrencias').select('*').eq('rdo_id', rdoId),
supabase.from('rdo_anexos').select('*').eq('rdo_id', rdoId),
supabase.from('rdo_inspecoes_solda').select('*').eq('rdo_id', rdoId),
supabase.from('rdo_verificacoes_torque').select('*').eq('rdo_id', rdoId)
]);
// Montar objeto completo seguindo o tipo RDOCompleto (adaptado)
// Nota: RDOCompleto em database.types.ts espera propriedades estritas.
// Faremos o cast ou montagem segura aqui.
const rdoCompleto: any = {
...rdoData,
atividades: atividades || [],
mao_obra: maoDeObra || [],
equipamentos: equipamentos || [],
ocorrencias: ocorrencias || [],
anexos: anexos || [],
inspecoes_solda: inspecoesSolda || [],
verificacoes_torque: verificacoesTorque || []
};
setRdo(rdoCompleto);
} catch (err: any) {
console.error('Erro ao buscar RDO:', err);
setError(err.message || 'Erro ao carregar RDO');
} finally {
setLoading(false);
}
}, [rdoId]);
useEffect(() => {
fetchRDO();
}, [fetchRDO]);
return { rdo, loading, error, refetch: fetchRDO };
};

View File

@@ -0,0 +1,210 @@
import { useEffect, useRef } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { supabase } from '../lib/supabase';
import { queryKeys, invalidateQueries } from '../lib/queryClient';
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);
useEffect(() => {
// Criar canal de subscription
channelRef.current = supabase
.channel('usuarios-changes')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'usuarios',
},
(payload) => {
console.log('Usuário alterado:', payload);
// Invalidar queries relacionadas a usuários
invalidateQueries.users();
// Se for um usuário específico, invalidar também
if (payload.new && typeof payload.new === 'object' && 'id' in payload.new) {
queryClient.invalidateQueries({
queryKey: queryKeys.users.detail(payload.new.id as string)
});
}
}
)
.subscribe();
// Cleanup na desmontagem
return () => {
if (channelRef.current) {
supabase.removeChannel(channelRef.current);
}
};
}, [queryClient]);
return channelRef.current;
};
// Hook para sincronização em tempo real de obras
export const useObrasRealtimeSync = () => {
const queryClient = useQueryClient();
const channelRef = useRef<RealtimeChannel | null>(null);
useEffect(() => {
channelRef.current = supabase
.channel('obras-changes')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'obras',
},
(payload) => {
console.log('Obra alterada:', payload);
// Invalidar queries relacionadas a obras
invalidateQueries.obras();
// Se for uma obra específica, invalidar também
if (payload.new && typeof payload.new === 'object' && 'id' in payload.new) {
queryClient.invalidateQueries({
queryKey: queryKeys.obras.detail(payload.new.id as string)
});
// Invalidar RDOs da obra também
invalidateQueries.rdosByObra(payload.new.id as string);
}
}
)
.subscribe();
return () => {
if (channelRef.current) {
supabase.removeChannel(channelRef.current);
}
};
}, [queryClient]);
return channelRef.current;
};
// Hook para sincronização em tempo real de RDOs
export const useRdosRealtimeSync = (obraId?: string) => {
const queryClient = useQueryClient();
const channelRef = useRef<RealtimeChannel | null>(null);
useEffect(() => {
channelRef.current = supabase
.channel('rdos-changes')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'rdos',
filter: obraId ? `obra_id=eq.${obraId}` : undefined,
},
(payload) => {
console.log('RDO alterado:', payload);
// Invalidar queries relacionadas a RDOs
invalidateQueries.rdos();
if (payload.new && typeof payload.new === 'object') {
const newRdo = payload.new as any;
// Invalidar RDO específico
if ('id' in newRdo) {
queryClient.invalidateQueries({
queryKey: queryKeys.rdos.detail(newRdo.id)
});
}
// Invalidar RDOs da obra
if ('obra_id' in newRdo) {
invalidateQueries.rdosByObra(newRdo.obra_id);
}
// Invalidar RDOs do usuário
if ('usuario_id' in newRdo) {
queryClient.invalidateQueries({
queryKey: queryKeys.rdos.byUser(newRdo.usuario_id)
});
}
}
}
)
.subscribe();
return () => {
if (channelRef.current) {
supabase.removeChannel(channelRef.current);
}
};
}, [queryClient, obraId]);
return channelRef.current;
};
// Hook principal para sincronização completa
export const useRealtimeSync = (options?: {
enableUsers?: boolean;
enableObras?: boolean;
enableRdos?: boolean;
obraId?: string;
}) => {
const {
enableUsers = true,
enableObras = true,
enableRdos = true,
obraId,
} = options || {};
const usersChannel = useUsersRealtime();
const obrasChannel = useObrasRealtimeSync();
const rdosChannel = useRdosRealtimeSync(obraId);
// Retornar status das conexões
return {
usersChannel: enableUsers ? usersChannel : null,
obrasChannel: enableObras ? obrasChannel : null,
rdosChannel: enableRdos ? rdosChannel : null,
isConnected: {
users: enableUsers && usersChannel?.state === 'joined',
obras: enableObras && obrasChannel?.state === 'joined',
rdos: enableRdos && rdosChannel?.state === 'joined',
},
};
};
// Hook para monitorar status de conectividade
export const useConnectionStatus = () => {
const queryClient = useQueryClient();
useEffect(() => {
const handleOnline = () => {
console.log('Conexão restaurada - invalidando queries');
// Quando voltar online, invalidar todas as queries para sincronizar
invalidateQueries.all();
};
const handleOffline = () => {
console.log('Conexão perdida');
};
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, [queryClient]);
return {
isOnline: navigator.onLine,
};
};

141
src/hooks/useSocialAuth.ts Normal file
View File

@@ -0,0 +1,141 @@
/**
* Hook para Social Authentication
*
* Gerencia login com Google e Microsoft via Supabase OAuth
*/
import { useState } from 'react';
import { supabase } from '../lib/supabase';
import type { Provider } from '@supabase/supabase-js';
export interface SocialAuthError {
message: string;
provider: Provider;
}
export const useSocialAuth = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<SocialAuthError | null>(null);
/**
* Inicia login com provider social
*/
const signInWithProvider = async (provider: Provider) => {
try {
setLoading(true);
setError(null);
const { data, error: authError } = await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: `${window.location.origin}/auth/callback`,
queryParams: {
access_type: 'offline',
prompt: 'consent',
},
},
});
if (authError) throw authError;
// O Supabase redireciona automaticamente para o provider
// O callback será tratado em /auth/callback
return { data, error: null };
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Erro ao fazer login';
setError({ message: errorMessage, provider });
return { data: null, error: errorMessage };
} finally {
setLoading(false);
}
};
/**
* Login com Google
*/
const signInWithGoogle = () => signInWithProvider('google');
/**
* Login com Microsoft
*/
const signInWithMicrosoft = () => signInWithProvider('azure');
/**
* Vincula conta social a usuário existente
*/
const linkProvider = async (provider: Provider) => {
try {
setLoading(true);
setError(null);
const { data, error: linkError } = await supabase.auth.linkIdentity({
provider,
});
if (linkError) throw linkError;
return { data, error: null };
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Erro ao vincular conta';
setError({ message: errorMessage, provider });
return { data: null, error: errorMessage };
} finally {
setLoading(false);
}
};
/**
* Remove vinculação de conta social
*/
const unlinkProvider = async (provider: Provider) => {
try {
setLoading(true);
setError(null);
const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error('Usuário não autenticado');
// Encontrar identity do provider
const identity = user.identities?.find(id => id.provider === provider);
if (!identity) {
throw new Error(`Conta ${provider} não vinculada`);
}
const { data, error: unlinkError } = await supabase.auth.unlinkIdentity(identity);
if (unlinkError) throw unlinkError;
return { data, error: null };
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Erro ao desvincular conta';
setError({ message: errorMessage, provider });
return { data: null, error: errorMessage };
} finally {
setLoading(false);
}
};
/**
* Verifica se usuário tem provider vinculado
*/
const hasProvider = async (provider: Provider): Promise<boolean> => {
const { data: { user } } = await supabase.auth.getUser();
return user?.identities?.some(id => id.provider === provider) ?? false;
};
return {
loading,
error,
signInWithGoogle,
signInWithMicrosoft,
signInWithProvider,
linkProvider,
unlinkProvider,
hasProvider,
};
};

View File

@@ -0,0 +1,148 @@
import { useState, useEffect } from 'react';
import { supabase } from '../lib/supabase';
// import { useConfigStore } from '../stores/configStore';
// import type { ConfigItem, CondicaoClimatica } from '../stores/configStore';
/**
* Hook para carregar dados do Supabase e sincronizar com as stores locais
*/
export const useSupabaseData = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// const configStore = useConfigStore();
// Carregar tipos de atividade do Supabase
const loadTiposAtividade = async () => {
try {
console.log('🔄 Carregando tipos de atividade do Supabase...');
const { data, error } = await supabase
.from('tipos_atividade')
.select('*')
.eq('ativo', true)
.order('nome');
if (error) {
console.error('❌ Erro ao carregar tipos de atividade:', error);
throw error;
}
console.log('✅ Tipos de atividade carregados:', data);
// Converter dados do Supabase para o formato da store
// const tiposAtividade: ConfigItem[] = data?.map((item, index) => ({
// id: item.id.toString(),
// nome: item.nome,
// ativo: item.ativo,
// ordem: index + 1
// })) || [];
// Atualizar store com dados do Supabase
// configStore.tiposAtividade = tiposAtividade;
console.log('📊 Tipos de atividade carregados:', data);
return data || [];
} catch (err) {
console.error('❌ Erro ao carregar tipos de atividade:', err);
throw err;
}
};
// Carregar condições climáticas do Supabase
const loadCondicoesClimaticas = async () => {
try {
console.log('🔄 Carregando condições climáticas do Supabase...');
const { data, error } = await supabase
.from('condicoes_climaticas')
.select('*')
.eq('ativo', true)
.order('nome');
if (error) {
console.error('❌ Erro ao carregar condições climáticas:', error);
throw error;
}
console.log('✅ Condições climáticas carregadas:', data);
// Converter dados do Supabase para o formato da store
// const condicoesClimaticas: CondicaoClimatica[] = data?.map((item, index) => ({
// id: item.id.toString(),
// nome: item.nome,
// valor: item.valor || item.nome.toLowerCase().replace(/\s+/g, '_'),
// ativo: item.ativo,
// ordem: index + 1,
// descricao: item.descricao
// })) || [];
// Atualizar store com dados do Supabase
// configStore.condicoesClimaticas = condicoesClimaticas;
console.log('📊 Condições climáticas carregadas:', data);
return data || [];
} catch (err) {
console.error('❌ Erro ao carregar condições climáticas:', err);
throw err;
}
};
// Carregar funcionários do Supabase
const loadFuncionarios = async () => {
try {
console.log('🔄 Carregando funcionários do Supabase...');
const { data, error } = await supabase
.from('funcionarios')
.select('*')
.eq('ativo', true)
.order('nome');
if (error) {
console.error('❌ Erro ao carregar funcionários:', error);
throw error;
}
console.log('✅ Funcionários carregados:', data);
return data || [];
} catch (err) {
console.error('❌ Erro ao carregar funcionários:', err);
throw err;
}
};
// Função principal para carregar todos os dados
const loadAllData = async () => {
try {
setLoading(true);
setError(null);
console.log('🚀 Iniciando carregamento de todos os dados do Supabase...');
await Promise.all([
loadTiposAtividade(),
loadCondicoesClimaticas(),
loadFuncionarios()
]);
console.log('✅ Todos os dados carregados com sucesso!');
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Erro desconhecido';
console.error('❌ Erro ao carregar dados:', errorMessage);
setError(errorMessage);
} finally {
setLoading(false);
}
};
// Carregar dados automaticamente quando o hook é usado
useEffect(() => {
loadAllData();
}, []);
return {
loading,
error,
loadAllData,
loadTiposAtividade,
loadCondicoesClimaticas,
loadFuncionarios
};
};

29
src/hooks/useTheme.ts Normal file
View File

@@ -0,0 +1,29 @@
import { useState, useEffect } from 'react';
type Theme = 'light' | 'dark';
export function useTheme() {
const [theme, setTheme] = useState<Theme>(() => {
const savedTheme = localStorage.getItem('theme') as Theme;
if (savedTheme) {
return savedTheme;
}
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
});
useEffect(() => {
document.documentElement.classList.remove('light', 'dark');
document.documentElement.classList.add(theme);
localStorage.setItem('theme', theme);
}, [theme]);
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
return {
theme,
toggleTheme,
isDark: theme === 'dark'
};
}