First commit - backup RDOC
This commit is contained in:
12
src/hooks/index.ts
Normal file
12
src/hooks/index.ts
Normal 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';
|
||||
7
src/hooks/queries/index.ts
Normal file
7
src/hooks/queries/index.ts
Normal 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
|
||||
320
src/hooks/queries/useObras.ts
Normal file
320
src/hooks/queries/useObras.ts
Normal 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
|
||||
});
|
||||
};
|
||||
435
src/hooks/queries/useRdos.ts
Normal file
435
src/hooks/queries/useRdos.ts
Normal 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
|
||||
});
|
||||
}
|
||||
242
src/hooks/queries/useUsers.ts
Normal file
242
src/hooks/queries/useUsers.ts
Normal 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
438
src/hooks/useAuth.ts
Normal 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
363
src/hooks/useInviteCode.ts
Normal 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
163
src/hooks/useMFA.ts
Normal 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
474
src/hooks/useOffline.ts
Normal 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
82
src/hooks/useRDO.ts
Normal 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 };
|
||||
};
|
||||
210
src/hooks/useRealtimeSync.ts
Normal file
210
src/hooks/useRealtimeSync.ts
Normal 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
141
src/hooks/useSocialAuth.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
148
src/hooks/useSupabaseData.ts
Normal file
148
src/hooks/useSupabaseData.ts
Normal 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
29
src/hooks/useTheme.ts
Normal 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'
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user