🚀 Initial commit: Versão atual do TrackSteel APP

This commit is contained in:
2026-03-18 21:17:53 +00:00
commit bde410c9ad
633 changed files with 108150 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
return !!isMobile
}
+191
View File
@@ -0,0 +1,191 @@
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }
+125
View File
@@ -0,0 +1,125 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
export interface ApiKey {
id: string;
name: string;
key: string;
is_primary: boolean;
created_at: string;
updated_at: string;
}
export function useApiKeys() {
const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
const [loading, setLoading] = useState(true);
const fetchApiKeys = async () => {
try {
const { data, error } = await (supabase as any)
.from('api_keys')
.select('*')
.order('is_primary', { ascending: false })
.order('created_at', { ascending: true });
if (error) throw error;
setApiKeys(data || []);
} catch (error) {
console.error('Erro ao carregar chaves API:', error);
toast.error('Erro ao carregar chaves API');
} finally {
setLoading(false);
}
};
const saveApiKey = async (apiKey: Partial<ApiKey>) => {
try {
if (apiKey.id) {
// Atualizar chave existente
const { error } = await (supabase as any)
.from('api_keys')
.update({
name: apiKey.name,
key: apiKey.key,
updated_at: new Date().toISOString()
})
.eq('id', apiKey.id);
if (error) throw error;
} else {
// Criar nova chave
const { error } = await (supabase as any)
.from('api_keys')
.insert({
name: apiKey.name,
key: apiKey.key,
is_primary: false
});
if (error) throw error;
}
await fetchApiKeys();
toast.success('Chave API salva com sucesso!');
} catch (error) {
console.error('Erro ao salvar chave API:', error);
toast.error('Erro ao salvar chave API');
}
};
const setPrimaryKey = async (keyId: string) => {
try {
// Primeiro, remove o status de primary de todas as chaves
await (supabase as any)
.from('api_keys')
.update({ is_primary: false })
.neq('id', '00000000-0000-0000-0000-000000000000'); // Atualiza todas as linhas
// Depois, define a chave selecionada como primary
const { error } = await (supabase as any)
.from('api_keys')
.update({ is_primary: true })
.eq('id', keyId);
if (error) throw error;
await fetchApiKeys();
toast.success('Chave principal definida com sucesso!');
} catch (error) {
console.error('Erro ao definir chave principal:', error);
toast.error('Erro ao definir chave principal');
}
};
const deleteApiKey = async (keyId: string) => {
try {
const { error } = await (supabase as any)
.from('api_keys')
.delete()
.eq('id', keyId);
if (error) throw error;
await fetchApiKeys();
toast.success('Chave API removida com sucesso!');
} catch (error) {
console.error('Erro ao remover chave API:', error);
toast.error('Erro ao remover chave API');
}
};
useEffect(() => {
fetchApiKeys();
}, []);
return {
apiKeys,
loading,
saveApiKey,
setPrimaryKey,
deleteApiKey,
refetch: fetchApiKeys
};
}
@@ -0,0 +1,484 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { useAuth } from '@/hooks/useAuth';
import { toast } from 'sonner';
export interface ConflitoPeca {
peca_id: string;
marca: string;
quantidade_existente: number;
quantidade_romaneio: number;
fase?: string;
}
export interface ProcessoPulado {
nome: string;
ordem: number;
dataRetroativa: string;
processo_id: string;
}
export interface PecaComProcessosPulados {
peca_id: string;
marca: string;
quantidade_expedida: number;
processos_pulados: ProcessoPulado[];
processo_atual: string;
fase?: string;
}
export interface ProcessarApontamentoParams {
romaneioId: string;
numeroRomaneio: string;
ofNumber: string;
itens: Array<{
peca_id: string;
marca: string;
quantidade_expedida: number;
fase?: string;
}>;
resolucaoConflitos?: 'somar' | 'substituir' | 'cancelar';
criarProcessosRetroativos?: boolean;
}
export const useApontamentoAutomaticoRomaneio = () => {
const { user } = useAuth();
const queryClient = useQueryClient();
const verificarProcessamentoExistente = async (romaneioId: string, numeroRomaneio: string, ofNumber: string, itens: ProcessarApontamentoParams['itens']) => {
console.log('🔍 VERIFICAÇÃO ULTRA ROBUSTA - Checando se romaneio já foi processado...');
// VERIFICAÇÃO 1: Por logs de apontamentos automáticos (mais confiável)
const { data: logsExistentes, error: errorLogs } = await supabase
.from('logs_apontamentos_automaticos')
.select('id, status_operacao, total_pecas_processadas, created_at')
.eq('romaneio_id', romaneioId)
.eq('numero_romaneio', numeroRomaneio)
.eq('of_number', ofNumber)
.eq('status_operacao', 'sucesso')
.order('created_at', { ascending: false });
if (errorLogs) {
console.error('❌ Erro ao verificar logs existentes:', errorLogs);
}
console.log('📊 Logs de sucesso encontrados:', logsExistentes?.length || 0);
// Se há logs de sucesso, considera processado
if (logsExistentes && logsExistentes.length > 0) {
const logMaisRecente = logsExistentes[0];
console.log('🚫 ROMANEIO JÁ PROCESSADO - Log de sucesso encontrado:', logMaisRecente);
return true;
}
console.log('✅ ROMANEIO NÃO PROCESSADO - Pode prosseguir com apontamento');
return false;
};
const verificarSequenciaProcessos = async (ofNumber: string, itens: ProcessarApontamentoParams['itens']) => {
console.log('🔍 Verificando sequência de processos para peças...');
// Buscar processos ordenados
const { data: processos, error: processoError } = await supabase
.from('processos_fabricacao')
.select('id, nome, ordem')
.eq('ativo', true)
.order('ordem');
if (processoError || !processos) {
console.error('❌ Erro ao buscar processos:', processoError);
return { pecasOK: itens, pecasComProcessosPulados: [] };
}
const processoExpedicao = processos.find(p =>
p.nome?.toLowerCase().includes('expedição') ||
p.nome?.toLowerCase().includes('expedicao') ||
p.nome?.toLowerCase().includes('expedica')
);
if (!processoExpedicao) {
console.error('❌ Processo Expedição não encontrado');
return { pecasOK: [], pecasComProcessosPulados: [] };
}
const pecasOK: ProcessarApontamentoParams['itens'] = [];
const pecasComProcessosPulados: PecaComProcessosPulados[] = [];
for (const item of itens) {
console.log(`\n🔍 Analisando sequência da peça ${item.marca}:`);
// Buscar informações da peça (se tem componentes)
const { data: pecaInfo } = await supabase
.from('pecas')
.select('tem_componentes')
.eq('id', item.peca_id)
.single();
const temComponentes = pecaInfo?.tem_componentes || false;
// Buscar apontamentos existentes para esta peça
const { data: apontamentosExistentes } = await supabase
.from('apontamentos_producao')
.select(`
processo_id,
quantidade_produzida,
processos_fabricacao(nome, ordem)
`)
.eq('of_number', ofNumber)
.eq('peca_id', item.peca_id)
.eq('tipo_apontamento', 'peca');
if (!apontamentosExistentes) {
console.log(`❌ Erro ao buscar apontamentos da peça ${item.marca}`);
continue;
}
// Calcular quantidades por processo
const quantidadesPorProcesso = new Map<string, number>();
apontamentosExistentes.forEach(apt => {
const processoId = apt.processo_id;
const atual = quantidadesPorProcesso.get(processoId) || 0;
quantidadesPorProcesso.set(processoId, atual + apt.quantidade_produzida);
});
// Determinar qual é o último processo válido para esta peça
let ultimoProcessoValido = processoExpedicao;
for (let i = processoExpedicao.ordem - 1; i >= 1; i--) {
const processo = processos.find(p => p.ordem === i);
if (!processo) continue;
// Se é solda (ordem 3) e peça não tem componentes, pular
if (i === 3 && !temComponentes) {
console.log(`⏭️ Peça ${item.marca} sem componentes - pulando Solda (ordem 3)`);
continue;
}
ultimoProcessoValido = processo;
break;
}
// Verificar se foi apontada no último processo válido
const quantidadeUltimoProcesso = quantidadesPorProcesso.get(ultimoProcessoValido.id) || 0;
// Calcular quantidades já expedidas
const processoExpedicaoId = processoExpedicao.id;
const quantidadeJaExpedida = quantidadesPorProcesso.get(processoExpedicaoId) || 0;
// Calcular quantidade disponível para expedição
const quantidadeDisponivel = quantidadeUltimoProcesso - quantidadeJaExpedida;
const quantidadeParaApontar = Math.min(item.quantidade_expedida, quantidadeDisponivel);
console.log(`📊 Peça ${item.marca}:`);
console.log(` - Último processo válido: ${ultimoProcessoValido.nome} (${ultimoProcessoValido.ordem})`);
console.log(` - Quantidade no último processo: ${quantidadeUltimoProcesso}`);
console.log(` - Quantidade já expedida: ${quantidadeJaExpedida}`);
console.log(` - Quantidade disponível: ${quantidadeDisponivel}`);
console.log(` - Quantidade para apontar: ${quantidadeParaApontar}`);
if (quantidadeParaApontar <= 0) {
console.log(`⏭️ Peça ${item.marca} - sem quantidade disponível, pulando`);
continue;
}
// Verificar se precisa de processos retroativos
const processosQueDeviamTerSidoFeitos = [];
// Encontrar o processo em que a peça realmente está
let processoAtualEncontrado = null;
let ordemAtual = 0;
for (const processo of processos) {
if (processo.ordem >= processoExpedicao.ordem) break;
// Pular solda se não tem componentes
if (processo.ordem === 3 && !temComponentes) continue;
const qtdProcesso = quantidadesPorProcesso.get(processo.id) || 0;
if (qtdProcesso > 0) {
processoAtualEncontrado = processo;
ordemAtual = processo.ordem;
}
}
if (!processoAtualEncontrado) {
console.log(`❌ Peça ${item.marca} não encontrada em nenhum processo anterior`);
continue;
}
// Verificar processos que foram "pulados"
const processosPulados: ProcessoPulado[] = [];
const dataAtual = new Date();
for (const processo of processos) {
if (processo.ordem <= ordemAtual || processo.ordem >= processoExpedicao.ordem) continue;
// Pular solda se não tem componentes
if (processo.ordem === 3 && !temComponentes) continue;
const qtdProcesso = quantidadesPorProcesso.get(processo.id) || 0;
if (qtdProcesso === 0) {
// Calcular data retroativa (2 dias por processo anterior)
const diasRetroativos = (processoExpedicao.ordem - processo.ordem) * 2;
const dataRetroativa = new Date(dataAtual);
dataRetroativa.setDate(dataRetroativa.getDate() - diasRetroativos);
processosPulados.push({
nome: processo.nome,
ordem: processo.ordem,
processo_id: processo.id,
dataRetroativa: dataRetroativa.toISOString().split('T')[0]
});
}
}
if (processosPulados.length > 0) {
console.log(`⚠️ Peça ${item.marca} tem ${processosPulados.length} processos pulados`);
pecasComProcessosPulados.push({
peca_id: item.peca_id,
marca: item.marca,
quantidade_expedida: quantidadeParaApontar,
processos_pulados: processosPulados,
processo_atual: processoAtualEncontrado.nome,
fase: item.fase
});
} else {
console.log(`✅ Peça ${item.marca} está na sequência correta`);
// Atualizar a quantidade para refletir apenas o disponível
pecasOK.push({
...item,
quantidade_expedida: quantidadeParaApontar
});
}
}
console.log(`📋 Resultado da verificação:`);
console.log(` - Peças OK: ${pecasOK.length}`);
console.log(` - Peças com processos pulados: ${pecasComProcessosPulados.length}`);
return { pecasOK, pecasComProcessosPulados };
};
const processarApontamentos = useMutation({
mutationFn: async ({
romaneioId,
numeroRomaneio,
ofNumber,
itens,
resolucaoConflitos,
criarProcessosRetroativos = false
}: ProcessarApontamentoParams) => {
if (!user) throw new Error('Usuário não autenticado');
console.log('=== INICIANDO APONTAMENTO AUTOMÁTICO ===');
console.log('📦 Romaneio ID:', romaneioId);
console.log('📋 Romaneio:', numeroRomaneio);
console.log('🔧 OF:', ofNumber);
console.log('📊 Itens para processar:', itens.length);
console.log('🔄 Criar processos retroativos:', criarProcessosRetroativos);
try {
// VERIFICAÇÃO ULTRA ROBUSTA DE PROCESSAMENTO EXISTENTE
const jaProcessado = await verificarProcessamentoExistente(romaneioId, numeroRomaneio, ofNumber, itens);
if (jaProcessado) {
console.log('🚫 APONTAMENTO JÁ PROCESSADO - Operação cancelada');
toast.info(`️ Apontamento já processado para o romaneio ${numeroRomaneio}`);
return {
sucesso: false,
erro: 'Apontamento já processado para este romaneio',
totalProcessado: 0,
pecasProcessadas: []
};
}
// Verificar sequência de processos
const { pecasOK, pecasComProcessosPulados } = await verificarSequenciaProcessos(ofNumber, itens);
// Se há peças com processos pulados e não foi confirmado, retornar para confirmação
if (pecasComProcessosPulados.length > 0 && !criarProcessosRetroativos) {
console.log('⚠️ Peças com processos pulados detectadas, requerendo confirmação do usuário');
return {
precisaConfirmacaoProcessosPulados: true,
pecasOK,
pecasComProcessosPulados
};
}
// Buscar processo de Expedição
const { data: processosExpedicao, error: processoError } = await supabase
.from('processos_fabricacao')
.select('id, nome')
.or('nome.ilike.%expedição%,nome.ilike.%expedicao%,nome.ilike.%expedica%')
.eq('ativo', true);
if (processoError || !processosExpedicao || processosExpedicao.length === 0) {
console.error('❌ Erro ao buscar processo Expedição:', processoError);
throw new Error('Processo Expedição não encontrado');
}
const processoExpedicao = processosExpedicao[0];
console.log('✅ Processo Expedição encontrado:', processoExpedicao);
// Processar apontamentos
const apontamentosParaInserir = [];
const dataApontamento = new Date().toISOString().split('T')[0];
const observacaoEspecifica = `Apontamento automático via Romaneio ${numeroRomaneio} - ID: ${romaneioId}`;
let totalPecasProcessadas = 0;
const marcasProcessadas: string[] = [];
// 1. Se deve criar processos retroativos, criar primeiro
if (criarProcessosRetroativos && pecasComProcessosPulados.length > 0) {
console.log('🔙 Criando apontamentos retroativos...');
for (const pecaComProcessosPulados of pecasComProcessosPulados) {
for (const processoRetroativo of pecaComProcessosPulados.processos_pulados) {
apontamentosParaInserir.push({
of_number: ofNumber,
peca_id: pecaComProcessosPulados.peca_id,
processo_id: processoRetroativo.processo_id,
quantidade_produzida: pecaComProcessosPulados.quantidade_expedida,
data_apontamento: processoRetroativo.dataRetroativa,
observacoes: `Apontamento retroativo automático - ${observacaoEspecifica}`,
tipo_apontamento: 'peca',
created_by: user.id
});
}
// Adicionar também o apontamento de expedição para essa peça
apontamentosParaInserir.push({
of_number: ofNumber,
peca_id: pecaComProcessosPulados.peca_id,
processo_id: processoExpedicao.id,
quantidade_produzida: pecaComProcessosPulados.quantidade_expedida,
data_apontamento: dataApontamento,
observacoes: observacaoEspecifica,
tipo_apontamento: 'peca',
created_by: user.id
});
totalPecasProcessadas++;
marcasProcessadas.push(pecaComProcessosPulados.marca);
}
}
// 2. Processar peças que estão na sequência correta
for (const item of pecasOK) {
apontamentosParaInserir.push({
of_number: ofNumber,
peca_id: item.peca_id,
processo_id: processoExpedicao.id,
quantidade_produzida: item.quantidade_expedida,
data_apontamento: dataApontamento,
observacoes: observacaoEspecifica,
tipo_apontamento: 'peca',
created_by: user.id
});
totalPecasProcessadas++;
marcasProcessadas.push(item.marca);
}
if (apontamentosParaInserir.length === 0) {
console.log('️ Nenhum apontamento para inserir');
return {
sucesso: true,
totalProcessado: 0,
pecasProcessadas: [],
mensagem: 'Nenhuma peça elegível para apontamento automático'
};
}
console.log('📝 Inserindo', apontamentosParaInserir.length, 'apontamentos');
// Inserir apontamentos
const { error: insertError } = await supabase
.from('apontamentos_producao')
.insert(apontamentosParaInserir);
if (insertError) {
console.error('❌ Erro ao inserir apontamentos:', insertError);
throw new Error(`Erro ao inserir apontamentos: ${insertError.message}`);
}
// Registrar log de sucesso IMEDIATAMENTE após inserção bem-sucedida
const { error: logError } = await supabase
.from('logs_apontamentos_automaticos')
.insert({
romaneio_id: romaneioId,
numero_romaneio: numeroRomaneio,
of_number: ofNumber,
total_pecas_processadas: totalPecasProcessadas,
status_operacao: 'sucesso',
usuario_executou: user.id
});
if (logError) {
console.warn('⚠️ Erro ao registrar log (apontamento foi feito):', logError);
}
console.log('=== APONTAMENTO AUTOMÁTICO CONCLUÍDO COM SUCESSO ===');
return {
sucesso: true,
totalProcessado: totalPecasProcessadas,
pecasProcessadas: marcasProcessadas,
apontamentosRetroativosCriados: criarProcessosRetroativos ?
pecasComProcessosPulados.reduce((sum, p) => sum + p.processos_pulados.length, 0) : 0
};
} catch (error: any) {
console.error('=== ERRO NO APONTAMENTO AUTOMÁTICO ===');
console.error('❌ Erro:', error.message);
// Registrar log de erro
await supabase
.from('logs_apontamentos_automaticos')
.insert({
romaneio_id: romaneioId,
numero_romaneio: numeroRomaneio,
of_number: ofNumber,
total_pecas_processadas: 0,
status_operacao: 'erro',
detalhes_erro: error.message,
usuario_executou: user.id
});
throw error;
}
},
onSuccess: (result) => {
if (result.sucesso) {
queryClient.invalidateQueries({ queryKey: ['apontamentos-producao'] });
queryClient.invalidateQueries({ queryKey: ['pecas-pintura'] });
const marcas = result.pecasProcessadas?.join(', ') || '';
let mensagem = `✅ Apontamento automático concluído! ${result.totalProcessado} peças apontadas no processo Expedição`;
if (result.apontamentosRetroativosCriados && result.apontamentosRetroativosCriados > 0) {
mensagem += ` (${result.apontamentosRetroativosCriados} apontamentos retroativos criados)`;
}
if (marcas) {
mensagem += `: ${marcas}`;
}
toast.success(mensagem);
} else if (result.erro) {
toast.info(`${result.erro}`);
}
},
onError: (error: any) => {
console.error('❌ Erro no apontamento automático:', error);
toast.error(`❌ Erro no apontamento automático: ${error.message}`);
}
});
return {
processarApontamentos,
verificarSequenciaProcessos,
loading: processarApontamentos.isPending
};
};
+228
View File
@@ -0,0 +1,228 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { getSystemDateString, getDateOffsetString } from '@/utils/dateTimeUtils';
interface ApontamentoDiarioData {
ofNumber: string;
processo: string;
peso: number;
percentage: number;
}
interface ProcessoData {
processo: string;
total: number;
ofs: ApontamentoDiarioData[];
}
type PeriodoType = 'diario' | 'ultimos7' | 'ultimos15' | 'ultimos30' | 'projecao7';
interface ChartDataResult {
data: ProcessoData[];
effectiveDate: string;
}
export const useApontamentoDiarioChart = (periodo: PeriodoType) => {
const [result, setResult] = useState<ChartDataResult>({ data: [], effectiveDate: '' });
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchData = async () => {
setLoading(true);
setError(null);
try {
console.log('=== INICIANDO BUSCA DE DADOS PARA GRÁFICO DIÁRIO ===');
console.log('Período selecionado:', periodo);
// Calcular datas baseadas no período usando o utilitário de data do sistema
const hoje = getSystemDateString();
let dataInicio: string;
let dataFim: string = hoje;
let effectiveDate: string;
console.log('Data atual do sistema (hoje):', hoje);
switch (periodo) {
case 'diario':
// Para o período diário, usar a data atual (hoje) como data efetiva
dataInicio = hoje;
dataFim = hoje;
effectiveDate = hoje;
console.log('Modo diário - usando data atual:', effectiveDate);
break;
case 'ultimos7':
dataInicio = getDateOffsetString(-7);
effectiveDate = hoje;
break;
case 'ultimos15':
dataInicio = getDateOffsetString(-15);
effectiveDate = hoje;
break;
case 'ultimos30':
dataInicio = getDateOffsetString(-30);
effectiveDate = hoje;
break;
case 'projecao7':
// Para projeção, pegar dados dos últimos 14 dias para calcular média
dataInicio = getDateOffsetString(-14);
effectiveDate = hoje;
break;
default:
dataInicio = getDateOffsetString(-30);
effectiveDate = hoje;
}
console.log('Data início:', dataInicio);
console.log('Data fim:', dataFim);
console.log('Data efetiva para relatório:', effectiveDate);
// Consultar apontamentos com joins para obter pesos
const { data: apontamentos, error: apontamentosError } = await supabase
.from('apontamentos_producao')
.select(`
of_number,
data_apontamento,
quantidade_produzida,
tipo_apontamento,
processo:processos_fabricacao(nome, ordem),
peca:pecas(peso_unitario),
componente:componentes_peca(peso_unitario)
`)
.gte('data_apontamento', dataInicio)
.lte('data_apontamento', dataFim)
.order('of_number')
.order('data_apontamento');
if (apontamentosError) {
throw apontamentosError;
}
console.log('Apontamentos encontrados:', apontamentos?.length || 0);
console.log('Dados por OF:', apontamentos?.reduce((acc, apt) => {
acc[apt.of_number] = (acc[apt.of_number] || 0) + 1;
return acc;
}, {} as Record<string, number>));
if (!apontamentos || apontamentos.length === 0) {
console.log('Nenhum apontamento encontrado para o período');
setResult({ data: [], effectiveDate });
return;
}
// Processar dados agrupando por OF e processo
const dadosProcessados = new Map<string, Map<string, number>>();
apontamentos.forEach(apontamento => {
const ofNumber = apontamento.of_number;
const processoNome = apontamento.processo?.nome || 'Processo Desconhecido';
// Calcular peso baseado no tipo de apontamento
let pesoUnitario = 0;
if (apontamento.tipo_apontamento === 'componente' && apontamento.componente?.peso_unitario) {
pesoUnitario = Number(apontamento.componente.peso_unitario);
} else if (apontamento.tipo_apontamento === 'peca' && apontamento.peca?.peso_unitario) {
pesoUnitario = Number(apontamento.peca.peso_unitario);
}
let pesoTotal = pesoUnitario * Number(apontamento.quantidade_produzida);
// Para projeção, calcular média diária e multiplicar por 7
if (periodo === 'projecao7') {
const diasPeriodo = Math.ceil((new Date(dataFim).getTime() - new Date(dataInicio).getTime()) / (1000 * 60 * 60 * 24));
pesoTotal = (pesoTotal / diasPeriodo) * 7;
}
// Inicializar mapas se não existirem
if (!dadosProcessados.has(processoNome)) {
dadosProcessados.set(processoNome, new Map());
}
const processoMap = dadosProcessados.get(processoNome)!;
const pesoAtual = processoMap.get(ofNumber) || 0;
processoMap.set(ofNumber, pesoAtual + pesoTotal);
console.log(`OF ${ofNumber} - Processo ${processoNome}: +${pesoTotal.toFixed(2)} kg`);
});
// Converter para formato do gráfico
const processosOrdenados = ['Detalhamento', 'Corte', 'Solda', 'Pintura/Galv', 'Expedição', 'Montagem', 'Aceite/DB', 'Concluído'];
const processedData: ProcessoData[] = [];
processosOrdenados.forEach(processoNome => {
const processoData = dadosProcessados.get(processoNome);
if (processoData && processoData.size > 0) {
const ofsData: ApontamentoDiarioData[] = [];
let totalProcesso = 0;
// Converter Map para array e calcular total
for (const [ofNumber, peso] of processoData) {
if (peso > 0) {
ofsData.push({
ofNumber,
processo: processoNome,
peso: Math.round(peso),
percentage: 0 // Será calculado após somar o total
});
totalProcesso += peso;
}
}
// Calcular percentuais
ofsData.forEach(ofData => {
ofData.percentage = totalProcesso > 0 ? (ofData.peso / totalProcesso) * 100 : 0;
});
// Ordenar por OF
ofsData.sort((a, b) => a.ofNumber.localeCompare(b.ofNumber));
processedData.push({
processo: processoNome === 'Detalhamento' ? 'DETALH.' :
processoNome === 'Pintura/Galv' ? 'PINT/GALV.' :
processoNome === 'Expedição' ? 'EXPED.' :
processoNome === 'Montagem' ? 'MONTAG.' :
processoNome === 'Aceite/DB' ? 'ACEITE/DB' :
processoNome === 'Concluído' ? 'CONCLUÍDO' :
processoNome.toUpperCase(),
total: Math.round(totalProcesso),
ofs: ofsData
});
console.log(`Processo ${processoNome}: ${Math.round(totalProcesso)} kg total, ${ofsData.length} OFs`);
}
});
console.log('=== RESULTADO FINAL ===');
console.log('Processos com dados:', processedData.length);
console.log('Data efetiva:', effectiveDate);
console.log('OFs únicas no período:', [...new Set(apontamentos.map(apt => apt.of_number))]);
processedData.forEach(item => {
console.log(`${item.processo}: ${item.total} kg (${item.ofs.length} OFs)`);
item.ofs.forEach(ofData => {
console.log(` - OF ${ofData.ofNumber}: ${ofData.peso} kg`);
});
});
setResult({ data: processedData, effectiveDate });
} catch (error) {
console.error('Erro ao buscar dados do gráfico:', error);
setError('Erro ao carregar dados do gráfico');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, [periodo]);
return {
data: result.data,
effectiveDate: result.effectiveDate,
loading,
error,
refetch: fetchData
};
};
+146
View File
@@ -0,0 +1,146 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
interface ApontamentoExpedicao {
of_number: string;
etapa_fase: string;
marca: string;
descricao: string;
quantidade_expedida: number;
peso_total_expedido: number;
}
interface PecaCompleta {
id: string;
of_number: string;
etapa_fase: string;
marca: string;
descricao: string;
quantidade_total: number;
peso_unitario: number;
peso_total: number;
}
// Função para ordenação numérica natural
const naturalSort = (a: string, b: string): number => {
const collator = new Intl.Collator(undefined, {
numeric: true,
sensitivity: 'base'
});
return collator.compare(a, b);
};
export const useApontamentosExpedicao = (ofNumber: string) => {
const [apontamentosExpedicao, setApontamentosExpedicao] = useState<ApontamentoExpedicao[]>([]);
const [pecasCompletas, setPecasCompletas] = useState<PecaCompleta[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchApontamentosExpedicao = async () => {
try {
setLoading(true);
// Buscar dados das peças da OF
const { data: pecasData, error: pecasError } = await supabase
.from('pecas')
.select('*')
.eq('of_number', ofNumber);
if (pecasError) throw pecasError;
// Mapear os dados para o formato esperado
const pecasFormatadas = (pecasData || []).map(peca => ({
id: peca.id,
of_number: peca.of_number,
etapa_fase: peca.etapa_fase || '',
marca: peca.marca,
descricao: peca.descricao || '',
quantidade_total: peca.quantidade || 0,
peso_unitario: peca.peso_unitario || 0,
peso_total: peca.peso_total || 0
}));
// Ordenar peças por etapa_fase e marca com ordenação numérica natural
pecasFormatadas.sort((a, b) => {
const faseComparison = naturalSort(a.etapa_fase, b.etapa_fase);
if (faseComparison !== 0) return faseComparison;
return naturalSort(a.marca, b.marca);
});
// Buscar itens de romaneio expedidos para esta OF apenas de romaneios entregues ou conferidos
const { data: itensRomaneio, error: itensError } = await supabase
.from('itens_romaneio_pecas')
.select(`
marca,
fase,
descricao,
quantidade_expedida,
peso_unitario,
romaneios_expedicao!inner(of_number, status)
`)
.eq('romaneios_expedicao.of_number', ofNumber)
.in('romaneios_expedicao.status', ['Entregue', 'Conferido em Obra']);
if (itensError) {
console.error('Erro ao buscar itens de romaneio:', itensError);
// Continuar sem os dados de expedição se não conseguir buscar
setPecasCompletas(pecasFormatadas);
setApontamentosExpedicao([]);
return;
}
console.log('Itens de romaneio encontrados (apenas entregues/conferidos):', itensRomaneio?.length || 0);
// Processar itens de romaneio agrupando por OF + Fase + Marca
const itensAgrupados = (itensRomaneio || []).reduce((acc, item) => {
const key = `${ofNumber}-${item.fase || ''}-${item.marca}`;
if (acc[key]) {
acc[key].quantidade_expedida += item.quantidade_expedida;
acc[key].peso_total_expedido += item.quantidade_expedida * item.peso_unitario;
} else {
acc[key] = {
of_number: ofNumber,
etapa_fase: item.fase || '',
marca: item.marca,
descricao: item.descricao || '',
quantidade_expedida: item.quantidade_expedida,
peso_total_expedido: item.quantidade_expedida * item.peso_unitario
};
}
return acc;
}, {} as Record<string, ApontamentoExpedicao>);
const apontamentosArray = Object.values(itensAgrupados);
// Ordenar apontamentos por etapa_fase e marca com ordenação numérica natural
apontamentosArray.sort((a, b) => {
const faseComparison = naturalSort(a.etapa_fase, b.etapa_fase);
if (faseComparison !== 0) return faseComparison;
return naturalSort(a.marca, b.marca);
});
setPecasCompletas(pecasFormatadas);
setApontamentosExpedicao(apontamentosArray);
} catch (error) {
console.error('Erro ao buscar apontamentos de expedição:', error);
toast.error('Erro ao carregar dados de expedição');
} finally {
setLoading(false);
}
};
if (ofNumber) {
fetchApontamentosExpedicao();
}
}, [ofNumber]);
return {
apontamentosExpedicao,
pecasCompletas,
loading
};
};
+120
View File
@@ -0,0 +1,120 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
export interface ApontamentoPecaObra {
id: string;
rdo_id: string;
marca_peca: string;
quantidade: number;
created_at: string;
}
export interface NovoApontamentoPeca {
rdo_id: string;
marca_peca: string;
quantidade: number;
}
export const useApontamentosPecaObra = (rdoId?: string) => {
return useQuery({
queryKey: ['apontamentos_peca_obra', rdoId],
queryFn: async () => {
if (!rdoId) return [];
const { data, error } = await supabase
.from('apontamentos_peca_obra')
.select('*')
.eq('rdo_id', rdoId)
.order('created_at', { ascending: false });
if (error) throw error;
return data as ApontamentoPecaObra[];
},
enabled: !!rdoId,
});
};
export const useCreateApontamentoPeca = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (apontamento: NovoApontamentoPeca) => {
const { data, error } = await supabase
.from('apontamentos_peca_obra')
.insert([apontamento])
.select()
.single();
if (error) throw error;
return data;
},
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ['apontamentos_peca_obra', variables.rdo_id] });
queryClient.invalidateQueries({ queryKey: ['pecas_expedidas'] });
toast.success('Apontamento de peça adicionado com sucesso!');
},
onError: (error) => {
toast.error('Erro ao adicionar apontamento: ' + error.message);
},
});
};
export const useUpdateApontamentoPeca = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, ...updates }: Partial<ApontamentoPecaObra> & { id: string }) => {
const { data, error } = await supabase
.from('apontamentos_peca_obra')
.update(updates)
.eq('id', id)
.select()
.single();
if (error) throw error;
return data;
},
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['apontamentos_peca_obra', data.rdo_id] });
queryClient.invalidateQueries({ queryKey: ['pecas_expedidas'] });
toast.success('Apontamento atualizado com sucesso!');
},
onError: (error) => {
toast.error('Erro ao atualizar apontamento: ' + error.message);
},
});
};
export const useDeleteApontamentoPeca = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (id: string) => {
// Primeiro buscar o apontamento para invalidar as queries corretas
const { data: apontamento } = await supabase
.from('apontamentos_peca_obra')
.select('rdo_id')
.eq('id', id)
.single();
const { error } = await supabase
.from('apontamentos_peca_obra')
.delete()
.eq('id', id);
if (error) throw error;
return apontamento;
},
onSuccess: (apontamento) => {
if (apontamento) {
queryClient.invalidateQueries({ queryKey: ['apontamentos_peca_obra', apontamento.rdo_id] });
queryClient.invalidateQueries({ queryKey: ['pecas_expedidas'] });
}
toast.success('Apontamento removido com sucesso!');
},
onError: (error) => {
toast.error('Erro ao remover apontamento: ' + error.message);
},
});
};
+504
View File
@@ -0,0 +1,504 @@
import { useState, useEffect, useCallback } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { useAuth } from '@/hooks/useAuth';
import { toast } from 'sonner';
// Função utilitária para retry de requisições
const retryRequest = async <T,>(
requestFn: () => Promise<T>,
maxRetries: number = 3,
delay: number = 1000
): Promise<T> => {
let lastError: Error;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
console.log(`🔄 Tentativa ${attempt}/${maxRetries}`);
return await requestFn();
} catch (error) {
lastError = error as Error;
console.warn(`⚠️ Tentativa ${attempt} falhou:`, error);
// Se é o último retry ou não é um erro de rede, não tenta novamente
if (attempt === maxRetries ||
(!lastError.message.includes('ERR_ABORTED') &&
!lastError.message.includes('Failed to fetch') &&
!lastError.message.includes('aborted'))) {
throw lastError;
}
// Aguardar antes da próxima tentativa
await new Promise(resolve => setTimeout(resolve, delay * attempt));
}
}
throw lastError!;
};
export interface ApontamentoProducao {
id: string;
of_number: string;
peca_id: string;
componente_id?: string;
tipo_apontamento: 'peca' | 'componente';
processo_id: string;
quantidade_produzida: number;
data_apontamento: string;
observacoes?: string;
created_at: string;
created_by?: string;
peca?: {
marca: string;
descricao: string;
peso_unitario: number;
etapa_fase: string;
};
processo?: {
nome: string;
ordem: number;
};
componente?: {
marca_componente: string;
descricao: string;
peso_unitario: number;
};
}
export interface ProcessoFabricacao {
id: string;
nome: string;
descricao?: string;
ordem: number;
ativo: boolean;
cor?: string;
}
export const useApontamentosProducao = () => {
const [apontamentos, setApontamentos] = useState<ApontamentoProducao[]>([]);
const [processos, setProcessos] = useState<ProcessoFabricacao[]>([]);
const [loading, setLoading] = useState(true);
const { user } = useAuth();
const fetchApontamentos = useCallback(async () => {
try {
console.log('🔍 Iniciando busca COMPLETA de apontamentos...');
// Verificar se o usuário está autenticado
const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError) {
console.error('❌ Erro de autenticação:', authError);
toast.error('Erro de autenticação: ' + authError.message);
return;
}
if (!user) {
console.warn('⚠️ Usuário não autenticado');
toast.error('Usuário não autenticado');
return;
}
console.log('✅ Usuário autenticado:', user.id);
const { count: totalCount, error: countError } = await retryRequest(async () => {
const result = await supabase
.from('apontamentos_producao')
.select('*', { count: 'exact', head: true });
return result;
});
if (countError) {
console.error('❌ Erro ao contar registros:', countError);
console.error('❌ Detalhes do erro:', {
message: countError.message,
details: countError.details,
hint: countError.hint,
code: countError.code
});
toast.error('Erro ao acessar dados: ' + countError.message);
throw countError;
}
console.log(`📊 TOTAL DE REGISTROS NA TABELA: ${totalCount}`);
// SEGUNDA VERIFICAÇÃO: Buscar TODOS os apontamentos sem qualquer limitação
let allApontamentos: any[] = [];
let pageNumber: number = 0;
const itemsPerPage: number = 1000; // Buscar em lotes de 1000 para evitar timeout
let hasMoreData: boolean = true;
while (hasMoreData) {
const startIndex: number = pageNumber * itemsPerPage;
const endIndex: number = (pageNumber + 1) * itemsPerPage - 1;
console.log(`📄 Buscando página ${pageNumber + 1} (registros ${startIndex + 1} a ${endIndex + 1})`);
const { data: pageData, error: pageError } = await retryRequest(async () => {
const result = await supabase
.from('apontamentos_producao')
.select(`
*,
peca:pecas(marca, descricao, peso_unitario, etapa_fase),
processo:processos_fabricacao(nome, ordem),
componente:componentes_peca(marca_componente, descricao, peso_unitario)
`)
.range(startIndex, endIndex)
.order('created_at', { ascending: false });
return result;
});
if (pageError) {
console.error('❌ Erro ao buscar página:', pageError);
throw pageError;
}
if (!pageData || pageData.length === 0) {
hasMoreData = false;
break;
}
allApontamentos = [...allApontamentos, ...pageData];
console.log(`✅ Página ${pageNumber + 1}: ${pageData.length} registros carregados. Total acumulado: ${allApontamentos.length}`);
// Se retornou menos que o itemsPerPage, chegamos ao fim
if (pageData.length < itemsPerPage) {
hasMoreData = false;
}
pageNumber++;
}
console.log(`🎯 BUSCA FINALIZADA: ${allApontamentos.length} de ${totalCount || 0} registros carregados`);
// Verificar especificamente OF B118
const b118Count: number = allApontamentos.filter(apt => apt.of_number === 'B118').length;
console.log(`🔍 OF B118: ${b118Count} apontamentos encontrados`);
// Verificar distribuição por OF
const ofCounts = allApontamentos.reduce((acc, apt) => {
const of = apt.of_number || 'SEM_OF';
acc[of] = (acc[of] || 0) + 1;
return acc;
}, {} as Record<string, number>);
console.log('📋 Distribuição por OF (primeiros 10):',
Object.entries(ofCounts)
.sort(([,a], [,b]) => (b as number) - (a as number))
.slice(0, 10)
);
// Cast the tipo_apontamento to the correct type
const typedData = allApontamentos.map(item => ({
...item,
tipo_apontamento: (item.tipo_apontamento || 'peca') as 'peca' | 'componente'
}));
setApontamentos(typedData);
console.log(`✅ DADOS CARREGADOS COMPLETAMENTE: ${typedData.length} apontamentos`);
} catch (error) {
console.error('❌ ERRO CRÍTICO ao buscar apontamentos:', error);
// Diagnóstico detalhado do erro
if (error instanceof Error) {
console.error('❌ Tipo de erro:', error.name);
console.error('❌ Mensagem:', error.message);
console.error('❌ Stack:', error.stack);
// Verificar se é um erro de rede
if (error.message.includes('ERR_ABORTED') || error.message.includes('aborted')) {
console.error('🚨 ERRO DE REDE DETECTADO: ERR_ABORTED');
toast.error('Erro de conexão: Requisição cancelada. Verifique sua conexão com a internet.');
} else if (error.message.includes('Failed to fetch')) {
console.error('🚨 ERRO DE FETCH DETECTADO');
toast.error('Erro de conexão: Falha ao conectar com o servidor.');
} else {
toast.error('Erro ao carregar apontamentos: ' + error.message);
}
} else {
console.error('❌ Erro desconhecido:', error);
toast.error('Erro desconhecido ao carregar apontamentos');
}
}
}, []);
const fetchProcessos = useCallback(async () => {
try {
console.log('Buscando processos...');
const { data, error } = await retryRequest(async () => {
const result = await supabase
.from('processos_fabricacao')
.select('*')
.eq('ativo', true)
.order('ordem');
return result;
});
if (error) {
console.error('Erro ao buscar processos:', error);
throw error;
}
console.log(`Processos carregados: ${data?.length || 0} registros`);
setProcessos(data || []);
} catch (error) {
console.error('Erro ao buscar processos:', error);
toast.error('Erro ao carregar processos');
}
}, []);
const buscarQuantidadeProcessada = useCallback(async (
marcaItem: string,
processoId: string,
ofNumber: string,
tipoItem: 'peca' | 'componente' = 'peca'
): Promise<number> => {
try {
let query = supabase
.from('apontamentos_producao')
.select('quantidade_produzida')
.eq('processo_id', processoId)
.eq('of_number', ofNumber)
.eq('tipo_apontamento', tipoItem);
if (tipoItem === 'componente') {
// Para componentes, buscar pela marca_componente
const { data: componentesData, error: componentesError } = await supabase
.from('componentes_peca')
.select('id')
.eq('marca_componente', marcaItem);
if (componentesError || !componentesData?.length) {
console.log(`Nenhum componente encontrado para marca: ${marcaItem}`);
return 0;
}
const componenteIds = componentesData.map(c => c.id);
query = query.in('componente_id', componenteIds);
} else {
// Para peças, buscar pela marca da peça
const { data: pecasData, error: pecasError } = await supabase
.from('pecas')
.select('id')
.eq('marca', marcaItem)
.eq('of_number', ofNumber);
if (pecasError || !pecasData?.length) {
console.log(`Nenhuma peça encontrada para marca: ${marcaItem} na OF: ${ofNumber}`);
return 0;
}
const pecaIds = pecasData.map(p => p.id);
query = query.in('peca_id', pecaIds);
}
const { data, error } = await query;
if (error) {
console.error('Erro ao buscar quantidade processada:', error);
return 0;
}
const totalProcessado: number = data?.reduce((sum, apt) => sum + apt.quantidade_produzida, 0) || 0;
console.log(`Quantidade processada para ${marcaItem} no processo ${processoId}: ${totalProcessado}`);
return totalProcessado;
} catch (error) {
console.error('Erro ao buscar quantidade processada:', error);
return 0;
}
}, []);
const criarApontamento = useCallback(async (apontamento: {
of_number: string;
peca_id?: string;
componente_id?: string;
tipo_apontamento: 'peca' | 'componente';
processo_id: string;
quantidade_produzida: number;
data_apontamento: string;
observacoes?: string;
}) => {
try {
if (!user) throw new Error('Usuário não autenticado');
console.log('🔄 Criando apontamento:', apontamento);
const insertData: any = {
of_number: apontamento.of_number,
tipo_apontamento: apontamento.tipo_apontamento,
processo_id: apontamento.processo_id,
quantidade_produzida: apontamento.quantidade_produzida,
data_apontamento: apontamento.data_apontamento,
observacoes: apontamento.observacoes || null,
created_by: user.id
};
// Para componentes, precisamos buscar o peca_id da peça pai
if (apontamento.tipo_apontamento === 'componente' && apontamento.componente_id) {
// Buscar a peça pai do componente
const { data: componenteData, error: componenteError } = await supabase
.from('componentes_peca')
.select('peca_id')
.eq('id', apontamento.componente_id)
.single();
if (componenteError || !componenteData) {
throw new Error('Erro ao buscar dados do componente');
}
insertData.componente_id = apontamento.componente_id;
insertData.peca_id = componenteData.peca_id; // Usar o peca_id da peça pai
} else {
insertData.peca_id = apontamento.peca_id;
insertData.componente_id = null;
}
// Inserir o apontamento
const { error } = await supabase
.from('apontamentos_producao')
.insert(insertData);
if (error) {
console.error('❌ Erro SQL:', error);
throw error;
}
console.log('✅ Apontamento criado com sucesso, forçando atualização completa...');
// FORÇA ATUALIZAÇÃO COMPLETA DOS DADOS
await new Promise(resolve => setTimeout(resolve, 500)); // Aguarda 500ms para garantir persistência
await fetchApontamentos();
// Log de verificação
console.log(`🔍 Verificando apontamento criado para OF ${apontamento.of_number}`);
toast.success('Apontamento registrado com sucesso!');
return { success: true };
} catch (error) {
console.error('❌ Erro ao criar apontamento:', error);
toast.error('Erro ao registrar apontamento: ' + (error as any).message);
return { success: false, error };
}
}, [user, fetchApontamentos]);
const criarProcesso = async (processo: Omit<ProcessoFabricacao, 'id'>) => {
try {
if (!user) throw new Error('Usuário não autenticado');
const { data, error } = await supabase
.from('processos_fabricacao')
.insert({
...processo,
created_by: user.id
})
.select()
.single();
if (error) throw error;
toast.success('Processo criado com sucesso!');
await fetchProcessos();
return { success: true, data };
} catch (error) {
console.error('Erro ao criar processo:', error);
toast.error('Erro ao criar processo');
return { success: false, error };
}
};
const atualizarProcesso = async (id: string, processo: Partial<ProcessoFabricacao>) => {
try {
const { error } = await supabase
.from('processos_fabricacao')
.update(processo)
.eq('id', id);
if (error) throw error;
toast.success('Processo atualizado com sucesso!');
await fetchProcessos();
return { success: true };
} catch (error) {
console.error('Erro ao atualizar processo:', error);
toast.error('Erro ao atualizar processo');
return { success: false, error };
}
};
const exportarModeloCSV = () => {
const headers = ['of_number', 'marca_peca', 'processo', 'quantidade_produzida', 'data_apontamento', 'observacoes'];
const exampleData = [
['B002', 'COLUNA-4', 'Detalhamento', '5', '2025-06-28', 'Exemplo de observação'],
['B002', 'VIGA-10', 'Corte', '3', '2025-06-28', ''],
['B003', 'PILAR-2', 'Expedição', '2', '2025-06-28', 'Peça conferida']
];
const csvContent = [headers, ...exampleData]
.map(row => row.map(cell => `"${cell}"`).join(','))
.join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', 'modelo_apontamento_producao.csv');
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
toast.success('Modelo CSV baixado com sucesso!');
};
const refetch = useCallback(async () => {
console.log('🔄 Executando refetch completo...');
setLoading(true);
try {
await Promise.all([fetchApontamentos(), fetchProcessos()]);
console.log('✅ Refetch completado com sucesso');
} catch (error) {
console.error('❌ Erro durante refetch:', error);
} finally {
setLoading(false);
}
}, [fetchApontamentos, fetchProcessos]);
useEffect(() => {
if (user) {
console.log('🚀 Iniciando carregamento de dados para usuário:', user.id);
setLoading(true);
// Adicionar timeout para detectar requisições que ficam pendentes
const timeoutId = setTimeout(() => {
console.warn('⚠️ TIMEOUT: Requisições demorando mais que 30 segundos');
toast.error('Timeout: Requisições demorando muito. Verifique sua conexão.');
}, 30000);
Promise.all([
fetchApontamentos(),
fetchProcessos()
]).finally(() => {
clearTimeout(timeoutId);
setLoading(false);
console.log('✅ Carregamento finalizado');
});
} else {
console.log('❌ Usuário não autenticado, não carregando dados');
}
}, [user, fetchApontamentos, fetchProcessos]);
return {
apontamentos,
processos,
loading,
criarApontamento,
criarProcesso,
atualizarProcesso,
exportarModeloCSV,
buscarQuantidadeProcessada,
refetch
};
};
@@ -0,0 +1,417 @@
import { useState, useCallback, useRef, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
import { OptimizedCache } from '@/utils/OptimizedCache';
interface ItemDisponivel {
id: string;
tipo: 'peca' | 'componente';
marca: string;
descricao: string;
peso_unitario: number;
quantidade_total: number;
quantidade_produzida: number;
quantidade_disponivel: number;
peca_pai?: string;
}
interface ItensDisponiveis {
pecasDisponiveis: ItemDisponivel[];
componentesDisponiveis: ItemDisponivel[];
}
interface ProcessoFabricacao {
id: string;
nome: string;
ordem: number;
cor?: string;
ativo: boolean;
}
interface ResultadoValidacao {
valido: boolean;
motivo?: string;
proximoProcesso?: ProcessoFabricacao;
}
interface ApontamentoHistorico {
id: string;
of_number: string;
processo_id: string;
peca_id?: string;
componente_id?: string;
quantidade_produzida: number;
data_apontamento: string;
tipo_apontamento: 'peca' | 'componente';
created_at: string;
}
export const useApontamentosProducaoOtimizado = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [processosOrdenados, setProcessosOrdenados] = useState<ProcessoFabricacao[]>([]);
// Cache refs para performance
const historicoCache = useRef<Map<string, ApontamentoHistorico[]>>(new Map());
const itensCache = useRef<Map<string, ItensDisponiveis>>(new Map());
const processosCache = useRef<Map<string, ProcessoFabricacao[]>>(new Map());
// Função para buscar itens disponíveis otimizada
const calcularItensDisponiveis = useCallback(async (ofNumber?: string, processoId?: string): Promise<ItensDisponiveis> => {
if (!ofNumber) {
return {
pecasDisponiveis: [],
componentesDisponiveis: []
};
}
const cacheKey = `itens_${ofNumber}_${processoId || 'all'}`;
try {
// Verificar cache primeiro
const cached = await OptimizedCache.get(cacheKey, 'ITENS_DISPONIVEIS');
if (cached) {
console.log('📦 Cache HIT - Itens disponíveis:', cacheKey);
return {
pecasDisponiveis: [],
componentesDisponiveis: []
};
}
console.log('🔄 Calculando itens disponíveis para OF:', ofNumber, 'Processo:', processoId);
const startTime = performance.now();
// Buscar apontamentos existentes
const { data: apontamentos } = await supabase
.from('apontamentos_producao')
.select('*')
.eq('of_number', ofNumber);
// Buscar peças da OF
const { data: pecas } = await supabase
.from('pecas')
.select('*')
.eq('of_number', ofNumber);
// Buscar componentes das peças
const { data: componentes } = await supabase
.from('componentes_peca')
.select(`
*,
peca:pecas!inner(of_number, marca)
`)
.eq('peca.of_number', ofNumber);
const resultado: ItensDisponiveis = {
pecasDisponiveis: [],
componentesDisponiveis: []
};
// Calcular peças disponíveis
if (pecas) {
pecas.forEach(peca => {
const apontamentosPeca = apontamentos?.filter(a =>
a.peca_id === peca.id && a.tipo_apontamento === 'peca'
) || [];
const quantidadeProduzida = apontamentosPeca.reduce((sum, a) => sum + (a.quantidade_produzida || 0), 0);
const quantidadeDisponivel = Math.max(0, (peca.quantidade || 0) - quantidadeProduzida);
if (quantidadeDisponivel > 0) {
resultado.pecasDisponiveis.push({
id: peca.id,
tipo: 'peca',
marca: peca.marca || '',
descricao: peca.descricao || '',
peso_unitario: peca.peso_unitario || 0,
quantidade_total: peca.quantidade || 0,
quantidade_produzida: quantidadeProduzida,
quantidade_disponivel: quantidadeDisponivel
});
}
});
}
// Calcular componentes disponíveis
if (componentes) {
componentes.forEach(componente => {
const apontamentosComponente = apontamentos?.filter(a =>
a.componente_id === componente.id && a.tipo_apontamento === 'componente'
) || [];
const quantidadeProduzida = apontamentosComponente.reduce((sum, a) => sum + (a.quantidade_produzida || 0), 0);
// Quantidade total considerando a peça pai
const pecaPai = pecas?.find(p => p.id === componente.peca_id);
const quantidadeTotal = (componente.quantidade_por_peca || 1) * (pecaPai?.quantidade || 0);
const quantidadeDisponivel = Math.max(0, quantidadeTotal - quantidadeProduzida);
if (quantidadeDisponivel > 0) {
resultado.componentesDisponiveis.push({
id: componente.id,
tipo: 'componente',
marca: componente.marca_componente || '',
descricao: componente.descricao || '',
peso_unitario: componente.peso_unitario || 0,
quantidade_total: quantidadeTotal,
quantidade_produzida: quantidadeProduzida,
quantidade_disponivel: quantidadeDisponivel,
peca_pai: pecaPai?.marca
});
}
});
}
// Armazenar no cache
OptimizedCache.set(cacheKey, resultado, 'ITENS_DISPONIVEIS');
const duration = performance.now() - startTime;
console.log(`⚡ Itens calculados em ${duration.toFixed(1)}ms:`, {
pecas: resultado.pecasDisponiveis.length,
componentes: resultado.componentesDisponiveis.length
});
return resultado;
} catch (error) {
console.error('❌ Erro ao calcular itens disponíveis:', error);
return {
pecasDisponiveis: [],
componentesDisponiveis: []
};
}
}, []);
// Função para buscar histórico de apontamentos
const carregarHistoricoOF = useCallback(async (ofNumber: string): Promise<ApontamentoHistorico[]> => {
if (!ofNumber) return [];
const cacheKey = `historico_${ofNumber}`;
try {
// Verificar cache primeiro
const cached = await OptimizedCache.get<ApontamentoHistorico[]>(cacheKey, 'HISTORICO_OF');
if (cached) {
console.log('📦 Cache HIT - Histórico OF:', ofNumber);
return cached;
}
console.log('🔄 Carregando histórico da OF:', ofNumber);
const { data: apontamentos } = await supabase
.from('apontamentos_producao')
.select(`
*,
peca:pecas(marca, descricao),
processo:processos_fabricacao(nome, ordem),
componente:componentes_peca(marca_componente, descricao)
`)
.eq('of_number', ofNumber)
.order('created_at', { ascending: false });
const historico: ApontamentoHistorico[] = (apontamentos || []).map(a => ({
id: a.id,
of_number: a.of_number,
processo_id: a.processo_id,
peca_id: a.peca_id,
componente_id: a.componente_id,
quantidade_produzida: a.quantidade_produzida,
data_apontamento: a.data_apontamento,
tipo_apontamento: (a.tipo_apontamento as 'peca' | 'componente') || 'peca',
created_at: a.created_at
}));
// Armazenar no cache
OptimizedCache.set(cacheKey, historico, 'HISTORICO_OF');
console.log(`📚 Histórico carregado: ${historico.length} apontamentos`);
return historico;
} catch (error) {
console.error('❌ Erro ao carregar histórico:', error);
return [];
}
}, []);
// Função para validar sequência de processos
const validarSequencia = useCallback(async (
ofNumber: string,
processoId: string,
tipoItem: 'peca' | 'componente',
itemId: string
): Promise<ResultadoValidacao> => {
const cacheKey = `validacao_${ofNumber}_${processoId}_${tipoItem}_${itemId}`;
try {
// Verificar cache primeiro
const cached = await OptimizedCache.get<ResultadoValidacao>(cacheKey, 'PROCESSOS');
if (cached) {
return cached;
}
// Se não há cache, retornar resultado padrão válido
const resultado: ResultadoValidacao = {
valido: true,
motivo: undefined
};
// Cache temporário (validações podem mudar rapidamente)
if (Math.random() > 0.1) { // 90% chance de cachear
OptimizedCache.set(cacheKey, resultado, 'PROCESSOS');
}
return resultado;
} catch (error) {
console.error('❌ Erro na validação de sequência:', error);
return {
valido: false,
motivo: 'Erro interno na validação'
};
}
}, []);
// Buscar processos ordenados
const carregarProcessos = useCallback(async () => {
const cacheKey = 'processos_ativos';
try {
// Verificar cache primeiro
const cached = await OptimizedCache.get<ProcessoFabricacao[]>(cacheKey, 'PROCESSOS');
if (cached) {
setProcessosOrdenados(cached);
return cached;
}
const { data: processos } = await supabase
.from('processos_fabricacao')
.select('*')
.eq('ativo', true)
.order('ordem');
const processosLista = processos || [];
// Armazenar no cache
OptimizedCache.set(cacheKey, processosLista, 'PROCESSOS');
setProcessosOrdenados(processosLista);
return processosLista;
} catch (error) {
console.error('❌ Erro ao carregar processos:', error);
setError('Erro ao carregar processos de fabricação');
return [];
}
}, []);
// Invalidar caches específicos
const invalidarCache = useCallback((ofNumber?: string) => {
if (ofNumber) {
// Invalidar caches específicos da OF
OptimizedCache.invalidatePattern(`_${ofNumber}_`);
OptimizedCache.invalidatePattern(`historico_${ofNumber}`);
OptimizedCache.invalidatePattern(`itens_${ofNumber}`);
} else {
// Limpar todos os caches
OptimizedCache.clear();
OptimizedCache.clear();
OptimizedCache.clear();
}
// Limpar cache refs
historicoCache.current.clear();
itensCache.current.clear();
processosCache.current.clear();
}, []);
// Limpar caches ao desmontar
useEffect(() => {
return () => {
invalidarCache();
};
}, [invalidarCache]);
// Função para obter estatísticas de performance
const obterEstatisticas = useCallback(() => {
try {
const cacheStats = OptimizedCache.getStats();
const performanceStats = OptimizedCache.getStats();
const memoryStats = OptimizedCache.getStats();
return {
cache: {
entries: cacheStats.memoryEntries,
storageEntries: cacheStats.localStorageEntries,
totalSize: cacheStats.totalSize
},
performance: {
totalOperations: performanceStats.memoryEntries,
averageTime: performanceStats.localStorageEntries,
cacheHitRate: performanceStats.totalSize
},
memory: {
historico: historicoCache.current.size,
itens: itensCache.current.size,
processos: processosCache.current.size
}
};
} catch (error) {
console.error('Erro ao obter estatísticas:', error);
return null;
}
}, []);
// Função para pré-carregar dados críticos
const precarregarDados = useCallback(async (ofNumber: string) => {
if (!ofNumber) return;
try {
setLoading(true);
// Pré-carregar em paralelo
const promises = [
carregarProcessos(),
carregarHistoricoOF(ofNumber),
calcularItensDisponiveis(ofNumber)
];
await Promise.all(promises);
} catch (error) {
console.error('❌ Erro no pré-carregamento:', error);
setError('Erro ao pré-carregar dados');
} finally {
setLoading(false);
}
}, [carregarProcessos, carregarHistoricoOF, calcularItensDisponiveis]);
// Carregar processos na inicialização
useEffect(() => {
carregarProcessos();
}, [carregarProcessos]);
return {
// Estados
loading,
error,
processosOrdenados,
// Funções principais
calcularItensDisponiveis,
carregarHistoricoOF,
validarSequencia,
carregarProcessos,
// Utilitários
invalidarCache,
precarregarDados,
obterEstatisticas,
// Limpeza
limparCache: invalidarCache
};
};
export default useApontamentosProducaoOtimizado;
+191
View File
@@ -0,0 +1,191 @@
import { useRef, useCallback } from 'react';
import { supabase } from '@/integrations/supabase/client';
interface ValidacaoProcesso {
processoId: string;
ordenacaoCorreta: boolean;
proximoProcessoEsperado?: string;
}
interface ResultadoValidacao {
valido: boolean;
erro?: string;
processosInvalidos?: ValidacaoProcesso[];
}
export const useApontamentosValidacao = () => {
const cacheApontamentos = useRef<Map<string, any[]>>(new Map());
const processosCache = useRef<Map<string, any[]>>(new Map());
const pecasCache = useRef<Map<string, any[]>>(new Map());
const idsProcessados = useRef<Map<string, any[]>>(new Map());
const validarSequenciaProcessos = useCallback(async (
ofNumber: string,
processoId: string,
tipoItem: 'peca' | 'componente',
itemId: string
): Promise<ResultadoValidacao> => {
try {
// Buscar processo atual e sua ordem
const { data: processoAtual } = await supabase
.from('processos_fabricacao')
.select('id, nome, ordem')
.eq('id', processoId)
.single();
if (!processoAtual) {
return {
valido: false,
erro: 'Processo não encontrado'
};
}
// Buscar apontamentos anteriores para este item
const query = supabase
.from('apontamentos_producao')
.select(`
id,
processo_id,
data_apontamento,
processos_fabricacao:processo_id (
id,
nome,
ordem
)
`)
.eq('of_number', ofNumber)
.eq('tipo_apontamento', tipoItem);
if (tipoItem === 'peca') {
query.eq('peca_id', itemId);
} else {
query.eq('componente_id', itemId);
}
const { data: apontamentosAnteriores } = await query
.order('data_apontamento', { ascending: true });
if (!apontamentosAnteriores || apontamentosAnteriores.length === 0) {
// Primeiro apontamento, sempre válido
return { valido: true };
}
// Verificar se a sequência está correta
const processosJaExecutados = apontamentosAnteriores.map(a => a.processos_fabricacao.ordem);
const ordemAtual = processoAtual.ordem;
// Verificar se existe algum processo com ordem maior que já foi executado
const temProcessoFuturoExecutado = processosJaExecutados.some(ordem => ordem > ordemAtual);
if (temProcessoFuturoExecutado) {
return {
valido: false,
erro: `Não é possível apontar para ${processoAtual.nome} (ordem ${ordemAtual}), pois já existem apontamentos para processos posteriores.`
};
}
// Verificar se todos os processos anteriores foram executados
const { data: processosAnteriores } = await supabase
.from('processos_fabricacao')
.select('id, nome, ordem')
.lt('ordem', ordemAtual)
.eq('ativo', true)
.order('ordem');
if (processosAnteriores && processosAnteriores.length > 0) {
const ordensNecessarias = processosAnteriores.map(p => p.ordem);
const ordensExecutadas = [...new Set(processosJaExecutados)];
const processosFaltantes = ordensNecessarias.filter(ordem => !ordensExecutadas.includes(ordem));
if (processosFaltantes.length > 0) {
const nomesFaltantes = processosAnteriores
.filter(p => processosFaltantes.includes(p.ordem))
.map(p => p.nome)
.join(', ');
return {
valido: false,
erro: `É necessário executar primeiro os processos: ${nomesFaltantes}`
};
}
}
return { valido: true };
} catch (error) {
console.error('Erro na validação de sequência:', error);
return {
valido: false,
erro: 'Erro interno na validação'
};
}
}, []);
const precarregarDados = useCallback(async (ofNumber: string) => {
try {
// Pré-carregar apontamentos da OF
const { data: apontamentos } = await supabase
.from('apontamentos_producao')
.select(`
*,
processos_fabricacao:processo_id (id, nome, ordem)
`)
.eq('of_number', ofNumber);
if (apontamentos) {
// Agrupar por processo para cache
const apontamentosPorProcesso = apontamentos.reduce((acc, apontamento) => {
const key = `${apontamento.processo_id}_${apontamento.tipo_apontamento}`;
if (!acc[key]) acc[key] = [];
acc[key].push(apontamento);
return acc;
}, {} as Record<string, any[]>);
Object.entries(apontamentosPorProcesso).forEach(([processoId, apontamentos]) => {
if (!cacheApontamentos.current) {
cacheApontamentos.current = new Map();
}
cacheApontamentos.current.set(processoId, apontamentos);
});
}
// Pré-carregar informações das peças da OF
const { data: pecas } = await supabase
.from('pecas')
.select('*')
.eq('of_number', ofNumber);
if (pecas) {
pecasCache.current?.set(ofNumber, pecas);
}
// Pré-carregar processos
const { data: processos } = await supabase
.from('processos_fabricacao')
.select('*')
.eq('ativo', true)
.order('ordem');
if (processos) {
processosCache.current?.set('ativos', processos);
}
} catch (error) {
console.error('Erro ao pré-carregar dados para validação:', error);
}
}, []);
const limparCache = useCallback(() => {
cacheApontamentos.current?.clear();
processosCache.current?.clear();
pecasCache.current?.clear();
idsProcessados.current?.clear();
}, []);
return {
validarSequenciaProcessos,
precarregarDados,
limparCache
};
};
+323
View File
@@ -0,0 +1,323 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { useAuth } from '@/hooks/useAuth';
import { useUserRole } from '@/hooks/useUserRole';
import { toast } from 'sonner';
export interface Atribuicao {
id: string;
user_id: string;
user_name: string;
user_email: string;
user_abbrev: string;
user_photo?: string;
attribution: string;
frequency: 'horaria' | '2xdia' | 'diaria' | '2xsemanal' | 'semanal' | 'quinzenal' | 'mensal';
method: 'impresso' | 'sistema' | 'sistema-impresso' | 'email' | 'verbal';
client: 'interno' | 'processo' | 'obra' | 'contrato' | 'geral';
importance: 'essencial' | 'estrategico' | 'suporte' | 'informativo';
duration: '<=1 hora' | '2 horas' | '4 horas' | '8 horas';
created_at: string;
updated_at: string;
}
export interface UserProfile {
id: string;
full_name: string;
email: string;
profile_image_url?: string;
}
export function useAtribuicoes() {
const { user } = useAuth();
const { isAdmin } = useUserRole();
const [atribuicoes, setAtribuicoes] = useState<Atribuicao[]>([]);
const [users, setUsers] = useState<UserProfile[]>([]);
const [loading, setLoading] = useState(false);
const [canManage, setCanManage] = useState(false);
useEffect(() => {
const checkPermissions = async () => {
if (!user) {
console.log('❌ Sem usuário autenticado');
setCanManage(false);
return;
}
console.log('🔍 Verificando permissões do usuário:', { userId: user.id, isAdmin });
if (isAdmin) {
console.log('✅ Usuário é admin - acesso total');
setCanManage(true);
return;
}
try {
// Verificar se tem função de diretoria
const { data: profile, error } = await supabase
.from('profiles')
.select(`
id,
full_name,
functions!inner(name)
`)
.eq('id', user.id)
.maybeSingle();
console.log('👤 Perfil do usuário:', profile);
if (error) {
console.error('❌ Erro ao buscar perfil:', error);
setCanManage(false);
} else if (profile?.functions?.name === 'Diretoria') {
console.log('✅ Usuário tem função de Diretoria - acesso de gerenciamento');
setCanManage(true);
} else {
console.log('👀 Usuário tem acesso apenas de visualização');
setCanManage(false);
}
} catch (error) {
console.error('❌ Erro ao verificar permissões:', error);
setCanManage(false);
}
};
checkPermissions();
}, [user, isAdmin]);
const fetchAtribuicoes = async () => {
if (!user) return;
try {
setLoading(true);
console.log('📋 Buscando atribuições...');
// Primeiro, buscar as atribuições
const { data: atribuicoesData, error: atribuicoesError } = await supabase
.from('atribuicoes')
.select('*')
.order('created_at', { ascending: false });
if (atribuicoesError) {
console.error('❌ Erro na query de atribuições:', atribuicoesError);
toast.error('Erro ao carregar atribuições');
return;
}
console.log('✅ Atribuições carregadas:', atribuicoesData?.length || 0);
if (!atribuicoesData || atribuicoesData.length === 0) {
console.log('📝 Nenhuma atribuição encontrada');
setAtribuicoes([]);
return;
}
// Depois, buscar os dados dos usuários
const userIds = [...new Set(atribuicoesData.map(attr => attr.user_id))];
console.log('👥 Buscando dados dos usuários:', userIds);
const { data: profilesData, error: profilesError } = await supabase
.from('profiles')
.select('id, full_name, email, profile_image_url')
.in('id', userIds);
if (profilesError) {
console.error('❌ Erro ao buscar profiles:', profilesError);
toast.error('Erro ao carregar dados dos usuários');
return;
}
console.log('✅ Profiles carregados:', profilesData?.length || 0);
// Criar um mapa de profiles para lookup rápido
const profilesMap = new Map();
profilesData?.forEach(profile => {
profilesMap.set(profile.id, profile);
});
// Transformar dados para o formato esperado
const transformedData: Atribuicao[] = atribuicoesData.map(item => {
const profile = profilesMap.get(item.user_id);
return {
id: item.id,
user_id: item.user_id,
user_name: profile?.full_name || 'Usuário não encontrado',
user_email: profile?.email || '',
user_abbrev: item.user_abbrev,
user_photo: profile?.profile_image_url,
attribution: item.attribution,
frequency: item.frequency as any,
method: item.method as any,
client: item.client as any,
importance: item.importance as any,
duration: item.duration as any,
created_at: item.created_at,
updated_at: item.updated_at
};
});
console.log('✅ Dados transformados:', transformedData.length);
setAtribuicoes(transformedData);
} catch (error) {
console.error('❌ Erro ao buscar atribuições:', error);
toast.error('Erro ao carregar atribuições');
} finally {
setLoading(false);
}
};
const fetchUsers = async () => {
if (!canManage) return;
try {
console.log('👥 Buscando usuários...');
const { data, error } = await supabase
.from('profiles')
.select('id, full_name, email, profile_image_url')
.eq('status', 'active')
.order('full_name');
if (error) {
console.error('❌ Erro ao buscar usuários:', error);
setUsers([]);
return;
}
console.log('✅ Usuários carregados:', data?.length || 0);
setUsers(data || []);
} catch (error) {
console.error('❌ Erro ao buscar usuários:', error);
setUsers([]);
}
};
const createAtribuicao = async (atribuicaoData: Omit<Atribuicao, 'id' | 'created_at' | 'updated_at' | 'user_name' | 'user_email' | 'user_photo'>) => {
try {
const { error } = await supabase
.from('atribuicoes')
.insert({
user_id: atribuicaoData.user_id,
user_abbrev: atribuicaoData.user_abbrev,
attribution: atribuicaoData.attribution,
frequency: atribuicaoData.frequency,
method: atribuicaoData.method,
client: atribuicaoData.client,
importance: atribuicaoData.importance,
duration: atribuicaoData.duration,
created_by: user?.id
});
if (error) throw error;
toast.success('Atribuição criada com sucesso');
fetchAtribuicoes();
} catch (error) {
console.error('Erro ao criar atribuição:', error);
toast.error('Erro ao criar atribuição');
}
};
const updateAtribuicao = async (id: string, atribuicaoData: Partial<Omit<Atribuicao, 'id' | 'created_at' | 'updated_at' | 'user_name' | 'user_email' | 'user_photo'>>) => {
try {
const updateData: any = {};
if (atribuicaoData.attribution !== undefined) updateData.attribution = atribuicaoData.attribution;
if (atribuicaoData.frequency !== undefined) updateData.frequency = atribuicaoData.frequency;
if (atribuicaoData.method !== undefined) updateData.method = atribuicaoData.method;
if (atribuicaoData.client !== undefined) updateData.client = atribuicaoData.client;
if (atribuicaoData.importance !== undefined) updateData.importance = atribuicaoData.importance;
if (atribuicaoData.duration !== undefined) updateData.duration = atribuicaoData.duration;
if (atribuicaoData.user_abbrev !== undefined) updateData.user_abbrev = atribuicaoData.user_abbrev;
const { error } = await supabase
.from('atribuicoes')
.update(updateData)
.eq('id', id);
if (error) throw error;
toast.success('Atribuição atualizada com sucesso');
fetchAtribuicoes();
} catch (error) {
console.error('Erro ao atualizar atribuição:', error);
toast.error('Erro ao atualizar atribuição');
}
};
const deleteAtribuicao = async (id: string) => {
try {
const { error } = await supabase
.from('atribuicoes')
.delete()
.eq('id', id);
if (error) throw error;
toast.success('Atribuição removida com sucesso');
fetchAtribuicoes();
} catch (error) {
console.error('Erro ao remover atribuição:', error);
toast.error('Erro ao remover atribuição');
}
};
const updateUserAbbrev = async (userId: string, newAbbrev: string) => {
try {
const { error } = await supabase
.from('atribuicoes')
.update({ user_abbrev: newAbbrev.toUpperCase().substring(0, 3) })
.eq('user_id', userId);
if (error) throw error;
toast.success('Identificação atualizada com sucesso');
fetchAtribuicoes();
} catch (error) {
console.error('Erro ao atualizar identificação:', error);
toast.error('Erro ao atualizar identificação');
}
};
// Effect separado para buscar dados apenas quando necessário
useEffect(() => {
console.log('🔄 useEffect fetchAtribuicoes:', { user: !!user, loading });
if (user && !loading) {
fetchAtribuicoes();
}
}, [user]);
// Effect separado para buscar usuários
useEffect(() => {
console.log('🔄 useEffect fetchUsers:', { canManage });
if (canManage) {
fetchUsers();
}
}, [canManage]);
console.log('🏠 Hook state:', {
user: !!user,
isAdmin,
canManage,
loading,
atribuicoesCount: atribuicoes.length,
usersCount: users.length
});
return {
atribuicoes,
users,
loading,
canManage,
createAtribuicao,
updateAtribuicao,
deleteAtribuicao,
updateUserAbbrev,
fetchAtribuicoes,
fetchUsers
};
}
+288
View File
@@ -0,0 +1,288 @@
import { useState, useCallback } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { naturalSort } from '@/utils/naturalSort';
interface InconsistenciaItem {
marca: string;
tipo: string;
descricao: string;
detalhes: any;
acaoSugerida?: string;
}
interface ResultadoAuditoria {
inconsistencias: {
processos: InconsistenciaItem[];
quantidades: InconsistenciaItem[];
expedicao: InconsistenciaItem[];
prioridades: InconsistenciaItem[];
estoque: InconsistenciaItem[];
};
verificacoesRealizadas: string[];
dataVerificacao: Date;
}
export const useAuditoriaInconsistencias = () => {
const [data, setData] = useState<ResultadoAuditoria | null>(null);
const [loading, setLoading] = useState(false);
const executarAuditoria = useCallback(async (ofNumber: string, processo?: string, faseOF?: string) => {
setLoading(true);
try {
console.log(`🔍 Iniciando auditoria para OF: ${ofNumber}, Processo: ${processo || 'todos'}, Fase: ${faseOF || 'todas'}`);
// Buscar dados necessários
const [pecasData, apontamentosData, romaneiosData, prioridadesData, processosData] = await Promise.all([
// Peças da OF filtradas por fase se especificada
supabase
.from('pecas')
.select('*')
.eq('of_number', ofNumber)
.then(({ data }) => {
if (!data) return [];
return faseOF ? data.filter(p => p.etapa_fase === faseOF) : data;
}),
// Apontamentos de produção
supabase
.from('apontamentos_producao')
.select(`
*,
processo:processos_fabricacao(nome, ordem),
peca:pecas(marca, etapa_fase, quantidade),
componente:componentes_peca(marca_componente)
`)
.eq('of_number', ofNumber)
.then(({ data }) => data || []),
// Romaneios de expedição
supabase
.from('itens_romaneio_pecas')
.select(`
*,
romaneio:romaneios_expedicao!inner(of_number)
`)
.eq('romaneio.of_number', ofNumber)
.then(({ data }) => data || []),
// Prioridades de fabricação - buscar corretamente as peças em prioridades
supabase
.from('itens_prioridade_fabricacao')
.select(`
*,
peca:pecas!inner(of_number, marca, etapa_fase),
prioridade_fabricacao:prioridades_fabricacao!inner(
of_number,
etapa_fase,
prioridade_config:prioridades_config(codigo, nome)
)
`)
.eq('peca.of_number', ofNumber)
.then(({ data }) => {
if (!data) return [];
return faseOF ? data.filter(item => item.peca?.etapa_fase === faseOF) : data;
}),
// Processos de fabricação
supabase
.from('processos_fabricacao')
.select('*')
.order('ordem')
.then(({ data }) => data || [])
]);
// Filtrar apontamentos por fase da OF se especificada
const apontamentosFiltrados = faseOF ?
apontamentosData.filter(a => a.peca?.etapa_fase === faseOF) :
apontamentosData;
// Filtrar por processo se especificado
const apontamentosProcessoFiltrados = processo ?
apontamentosFiltrados.filter(a => a.processo?.nome === processo) :
apontamentosFiltrados;
const inconsistencias: ResultadoAuditoria['inconsistencias'] = {
processos: [],
quantidades: [],
expedicao: [],
prioridades: [],
estoque: []
};
// 1. Verificar sequência de processos
pecasData.forEach(peca => {
const apontamentosPeca = apontamentosProcessoFiltrados.filter(a =>
a.tipo_apontamento === 'peca' && a.peca?.marca === peca.marca
);
// Ordenar apontamentos por ordem do processo
const apontamentosOrdenados = apontamentosPeca
.filter(a => a.processo?.ordem)
.sort((a, b) => a.processo.ordem - b.processo.ordem);
// Verificar se há processos pulados
for (let i = 1; i < apontamentosOrdenados.length; i++) {
const ordemAnterior = apontamentosOrdenados[i - 1].processo.ordem;
const ordemAtual = apontamentosOrdenados[i].processo.ordem;
if (ordemAtual - ordemAnterior > 1) {
// Verificar se a peça tem componentes para determinar se deve passar pela solda
const devePassarPelaSolda = peca.tem_componentes;
const processosPulados = [];
for (let ordem = ordemAnterior + 1; ordem < ordemAtual; ordem++) {
const processoEncontrado = processosData.find(p => p.ordem === ordem);
if (processoEncontrado) {
// Se não tem componentes e o processo é solda, não é inconsistência
if (!devePassarPelaSolda && processoEncontrado.nome.toLowerCase().includes('solda')) {
continue;
}
processosPulados.push(processoEncontrado.nome);
}
}
if (processosPulados.length > 0) {
inconsistencias.processos.push({
marca: peca.marca,
tipo: 'Processo Pulado',
descricao: `Peça pulou processo(s): ${processosPulados.join(', ')}`,
detalhes: {
processosPulados,
apontamentos: apontamentosOrdenados.map(a => ({
processo: a.processo.nome,
data: a.data_apontamento,
quantidade: a.quantidade_produzida
}))
},
acaoSugerida: 'Verificar se os processos intermediários foram executados'
});
}
}
}
});
// 2. Verificar quantidades inconsistentes
pecasData.forEach(peca => {
const apontamentosPeca = apontamentosProcessoFiltrados.filter(a =>
a.tipo_apontamento === 'peca' && a.peca?.marca === peca.marca
);
// Agrupar por processo
const quantidadesPorProcesso = apontamentosPeca.reduce((acc, apt) => {
const processo = apt.processo?.nome || 'Desconhecido';
acc[processo] = (acc[processo] || 0) + apt.quantidade_produzida;
return acc;
}, {} as Record<string, number>);
// Verificar se algum processo tem mais quantidade que o cadastrado
Object.entries(quantidadesPorProcesso).forEach(([processo, quantidade]) => {
if (quantidade > peca.quantidade) {
inconsistencias.quantidades.push({
marca: peca.marca,
tipo: 'Quantidade Excedente',
descricao: `Processo ${processo} tem ${quantidade} peças apontadas, mas cadastro tem ${peca.quantidade}`,
detalhes: {
quantidadeCadastrada: peca.quantidade,
quantidadeApontada: quantidade,
processo,
diferenca: quantidade - peca.quantidade
},
acaoSugerida: 'Verificar apontamentos duplicados ou corrigir quantidade no cadastro'
});
}
});
});
// 3. Verificar peças expedidas sem processos anteriores
const pecasExpedidas = romaneiosData.map(r => r.marca);
const pecasComProcessos = [...new Set(apontamentosProcessoFiltrados.map(a => a.peca?.marca).filter(Boolean))];
pecasExpedidas.forEach(marca => {
if (!pecasComProcessos.includes(marca)) {
const peca = pecasData.find(p => p.marca === marca);
if (peca) {
inconsistencias.expedicao.push({
marca,
tipo: 'Expedição sem Processos',
descricao: 'Peça expedida sem nenhum processo de produção apontado',
detalhes: {
romaneios: romaneiosData.filter(r => r.marca === marca)
},
acaoSugerida: 'Apontar processos de produção antes da expedição'
});
}
}
});
// 4. Verificar peças em múltiplas prioridades DIFERENTES (P1, P2, P3, P4)
console.log('🔍 Verificando prioridades duplicadas para peças:', prioridadesData.length, 'itens encontrados');
const pecasEmPrioridades = prioridadesData.reduce((acc, item) => {
const marca = item.peca?.marca;
const codigoPrioridade = item.prioridade_fabricacao?.prioridade_config?.codigo;
if (marca && codigoPrioridade) {
if (!acc[marca]) acc[marca] = new Set();
acc[marca].add(codigoPrioridade);
}
return acc;
}, {} as Record<string, Set<string>>);
Object.entries(pecasEmPrioridades).forEach(([marca, prioridades]) => {
if (prioridades.size > 1) {
const prioridadesArray = Array.from(prioridades);
inconsistencias.prioridades.push({
marca,
tipo: 'Múltiplas Prioridades',
descricao: `Peça está em ${prioridades.size} prioridades diferentes: ${prioridadesArray.join(', ')}`,
detalhes: {
prioridades: prioridadesArray,
detalhes: prioridadesData
.filter(item => item.peca?.marca === marca)
.map(item => ({
prioridade: item.prioridade_fabricacao?.prioridade_config?.codigo,
nome_prioridade: item.prioridade_fabricacao?.prioridade_config?.nome,
quantidade: item.quantidade_priorizada
}))
},
acaoSugerida: 'Remover peça das prioridades desnecessárias, mantendo apenas em uma'
});
}
});
// Aplicar ordenação natural
Object.keys(inconsistencias).forEach(categoria => {
(inconsistencias as any)[categoria].sort((a: any, b: any) => naturalSort(a.marca, b.marca));
});
const resultado: ResultadoAuditoria = {
inconsistencias,
verificacoesRealizadas: [
'Sequência de processos de produção',
'Consistência de quantidades por processo',
'Peças expedidas sem processos anteriores',
'Peças em múltiplas prioridades de fabricação diferentes',
'Duplicação de apontamentos no mesmo processo'
],
dataVerificacao: new Date()
};
console.log(`✅ Auditoria concluída: ${Object.values(inconsistencias).reduce((total, cat) => total + cat.length, 0)} inconsistências encontradas`);
setData(resultado);
} catch (error) {
console.error('❌ Erro na auditoria:', error);
setData(null);
} finally {
setLoading(false);
}
}, []);
return {
data,
loading,
executarAuditoria
};
};
+400
View File
@@ -0,0 +1,400 @@
import { useState, useEffect, createContext, useContext, ReactNode } from 'react';
import { User, Session } from '@supabase/supabase-js';
import { supabase } from '@/integrations/supabase/client';
interface AuthContextType {
user: User | null;
session: Session | null;
loading: boolean;
signIn: (email: string, password: string) => Promise<{ error: any }>;
signUp: (email: string, password: string) => Promise<{ error: any }>;
signOut: () => Promise<void>;
updatePassword: (password: string) => Promise<{ error: any }>;
isRecoveryFlow: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(true);
const [isRecoveryFlow, setIsRecoveryFlow] = useState(false);
const [authInitialized, setAuthInitialized] = useState(false);
// Verifica se o usuário é admin ou desenvolvedor (excluídos dos logs de sessão)
const isAdminOrDeveloper = async (userId: string): Promise<boolean> => {
try {
const { data: adminRoles, error: adminError } = await supabase
.from('user_roles')
.select('role')
.eq('user_id', userId)
.eq('role', 'admin')
.maybeSingle();
if (!adminError && adminRoles) {
return true;
}
const { data: profile, error: profileError } = await supabase
.from('profiles')
.select('function_id, functions(name)')
.eq('id', userId)
.maybeSingle();
if (profileError) {
return false;
}
if (profile?.functions?.name === 'Desenvolvedor') {
return true;
}
return false;
} catch {
return false;
}
};
// Processa tokens de recuperação de senha na URL
const processRecoveryTokens = () => {
try {
const currentUrl = window.location.href;
const urlObj = new URL(currentUrl);
const recoveryType = urlObj.searchParams.get('type');
const hashParams = new URLSearchParams(urlObj.hash.substring(1));
const accessToken = hashParams.get('access_token');
const refreshToken = hashParams.get('refresh_token');
const tokenType = hashParams.get('type');
const isRecovery = recoveryType === 'recovery' || tokenType === 'recovery' || (accessToken && refreshToken);
if (isRecovery) {
setIsRecoveryFlow(true);
if (accessToken && refreshToken) {
supabase.auth.setSession({
access_token: accessToken,
refresh_token: refreshToken
}).then(({ data, error }) => {
if (!error) {
setSession(data.session);
setUser(data.session?.user || null);
setIsRecoveryFlow(true);
}
const cleanUrl = `${window.location.origin}/auth?type=recovery`;
window.history.replaceState({}, '', cleanUrl);
});
}
return true;
}
return false;
} catch {
return false;
}
};
// Marca usuário como online no sistema
const setUserOnline = async (userId: string) => {
if (!authInitialized) return;
try {
const shouldSkipLogging = await isAdminOrDeveloper(userId);
if (shouldSkipLogging) return;
try {
await supabase.rpc('set_user_online', { user_id_param: userId });
} catch {
// RPC não encontrada — continua sem erro
}
} catch (error) {
console.error('Erro ao marcar usuário como online:', error);
}
};
const setUserOffline = async (userId: string) => {
if (!authInitialized) return;
try {
const shouldSkipLogging = await isAdminOrDeveloper(userId);
if (shouldSkipLogging) return;
try {
await supabase.rpc('set_user_offline', { user_id_param: userId });
} catch {
// RPC não encontrada — continua sem erro
}
} catch (error) {
console.error('Erro ao marcar usuário como offline:', error);
}
};
const startSessionLog = async (userId: string) => {
if (!authInitialized) return;
try {
const shouldSkipLogging = await isAdminOrDeveloper(userId);
if (shouldSkipLogging) return;
const { data, error } = await supabase
.from('user_session_logs')
.insert({
user_id: userId,
user_agent: navigator.userAgent,
is_active: true
})
.select()
.single();
if (error) {
console.error('Erro ao registrar início de sessão:', error);
} else if (data) {
localStorage.setItem('currentSessionId', data.id);
}
} catch (error) {
console.error('Erro inesperado ao registrar início de sessão:', error);
}
};
const endSessionLog = async () => {
if (!authInitialized) return;
try {
const sessionId = localStorage.getItem('currentSessionId');
if (sessionId) {
try {
await supabase.rpc('end_user_session', { session_id: sessionId });
} catch {
// RPC não encontrada — continua sem erro
}
localStorage.removeItem('currentSessionId');
}
} catch (error) {
console.error('Erro ao finalizar sessão:', error);
}
};
useEffect(() => {
let mounted = true;
const initializeAuth = async () => {
try {
const hasRecoveryTokens = processRecoveryTokens();
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => {
if (!mounted) return;
setSession(session);
setUser(session?.user ?? null);
if (event === 'SIGNED_IN' && session?.user) {
setTimeout(() => {
if (mounted && authInitialized) {
setUserOnline(session.user.id);
startSessionLog(session.user.id);
}
}, 100);
} else if (event === 'SIGNED_OUT') {
setIsRecoveryFlow(false);
const currentUser = user;
if (currentUser && mounted && authInitialized) {
setTimeout(() => {
setUserOffline(currentUser.id);
endSessionLog();
}, 100);
}
}
}
);
if (!hasRecoveryTokens) {
try {
const { data: { session } } = await supabase.auth.getSession();
if (mounted && session?.user) {
setSession(session);
setUser(session.user);
setTimeout(() => {
if (mounted) {
setAuthInitialized(true);
setUserOnline(session.user.id);
const existingSessionId = localStorage.getItem('currentSessionId');
if (!existingSessionId) {
startSessionLog(session.user.id);
}
}
}, 100);
} else if (mounted) {
setSession(null);
setUser(null);
setAuthInitialized(true);
}
} catch (sessionError) {
console.error('Erro ao verificar sessão:', sessionError);
if (mounted) {
setSession(null);
setUser(null);
setAuthInitialized(true);
}
}
} else {
setAuthInitialized(true);
}
if (mounted) {
setLoading(false);
}
return () => {
mounted = false;
subscription.unsubscribe();
};
} catch (error) {
console.error('Erro na inicialização da autenticação:', error);
if (mounted) {
setLoading(false);
setSession(null);
setUser(null);
setAuthInitialized(true);
}
}
};
const cleanup = initializeAuth();
return () => {
cleanup.then((cleanupFn) => {
if (cleanupFn) cleanupFn();
});
};
}, []);
// Limpeza periódica de usuários offline e listener de fechamento de janela
useEffect(() => {
let cleanupInterval: NodeJS.Timeout;
if (user && authInitialized) {
cleanupInterval = setInterval(async () => {
try {
await supabase.rpc('cleanup_offline_users');
} catch {
// Ignora se RPC não existir
}
}, 60000);
const handleBeforeUnload = () => {
if (user) {
const sessionId = localStorage.getItem('currentSessionId');
if (sessionId) {
try {
supabase.rpc('end_user_session', { session_id: sessionId });
} catch {
// Ignora no unload
}
localStorage.removeItem('currentSessionId');
}
}
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => {
if (cleanupInterval) {
clearInterval(cleanupInterval);
}
window.removeEventListener('beforeunload', handleBeforeUnload);
};
}
}, [user?.id, authInitialized]);
const signIn = async (email: string, password: string) => {
try {
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
return { error };
} catch (error) {
console.error('Erro crítico no login:', error);
return { error };
}
};
const signUp = async (email: string, password: string) => {
try {
const redirectUrl = `${window.location.origin}/`;
const { error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: redirectUrl
}
});
return { error };
} catch (error) {
console.error('Erro crítico no signup:', error);
return { error };
}
};
const signOut = async () => {
const currentUser = user;
if (currentUser && authInitialized) {
setTimeout(() => {
setUserOffline(currentUser.id);
endSessionLog();
}, 100);
}
setIsRecoveryFlow(false);
try {
await supabase.auth.signOut();
} catch (error) {
console.error('Erro no signOut:', error);
}
};
const updatePassword = async (password: string) => {
try {
const { error } = await supabase.auth.updateUser({
password: password
});
if (!error) {
setIsRecoveryFlow(false);
}
return { error };
} catch (error) {
console.error('Erro crítico ao atualizar senha:', error);
return { error };
}
};
const value = {
user,
session,
loading,
signIn,
signUp,
signOut,
updatePassword,
isRecoveryFlow,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
+156
View File
@@ -0,0 +1,156 @@
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
export interface BackupLog {
id: string;
operation_type: 'backup' | 'restore';
status: 'in_progress' | 'completed' | 'failed';
file_name: string;
file_size?: number;
tables_count?: number;
records_count?: number;
started_at: string;
completed_at?: string;
error_message?: string;
created_by: string;
}
export const useBackupManager = () => {
const queryClient = useQueryClient();
const [isBackingUp, setIsBackingUp] = useState(false);
const [isRestoring, setIsRestoring] = useState(false);
// Buscar logs de backup
const { data: backupLogs, isLoading: logsLoading } = useQuery({
queryKey: ['backup-logs'],
queryFn: async () => {
const { data, error } = await supabase
.from('backup_logs')
.select('*')
.order('created_at', { ascending: false });
if (error) throw error;
return data as BackupLog[];
},
});
// Criar backup
const createBackup = useMutation({
mutationFn: async () => {
setIsBackingUp(true);
const { data: { session } } = await supabase.auth.getSession();
if (!session) throw new Error('Não autenticado');
console.log('Iniciando backup...');
// Fazer a requisição para a função edge
const response = await fetch(`https://lwjppiicofojfcdfjsto.supabase.co/functions/v1/backup-database`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${session.access_token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorText = await response.text();
console.error('Erro na resposta:', errorText);
throw new Error(`Erro ao criar backup: ${response.status} ${response.statusText}`);
}
// Verificar se a resposta é um arquivo ZIP
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/zip')) {
// É um arquivo ZIP - fazer download
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
// Tentar obter o nome do arquivo do header Content-Disposition
const contentDisposition = response.headers.get('content-disposition');
let filename = `backup_${new Date().toISOString().split('T')[0]}.zip`;
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="([^"]+)"/);
if (filenameMatch) {
filename = filenameMatch[1];
}
}
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
return { success: true, filename };
} else {
// Resposta JSON com erro
const errorData = await response.json();
throw new Error(errorData.error || 'Erro desconhecido');
}
},
onSuccess: (data) => {
console.log('Backup concluído com sucesso');
toast.success(`Backup criado com sucesso! Arquivo: ${data.filename}`);
queryClient.invalidateQueries({ queryKey: ['backup-logs'] });
},
onError: (error: Error) => {
console.error('Erro ao criar backup:', error);
toast.error(`Erro ao criar backup: ${error.message}`);
},
onSettled: () => {
setIsBackingUp(false);
}
});
// Restaurar backup
const restoreBackup = useMutation({
mutationFn: async (file: File) => {
setIsRestoring(true);
const { data: { session } } = await supabase.auth.getSession();
if (!session) throw new Error('Não autenticado');
const formData = new FormData();
formData.append('file', file);
const { data, error } = await supabase.functions.invoke('restore-database', {
body: formData,
headers: {
'Authorization': `Bearer ${session.access_token}`,
}
});
if (error) {
throw new Error(error.message || 'Erro ao restaurar backup');
}
return data;
},
onSuccess: (data) => {
toast.success(`Backup restaurado com sucesso! ${data.tablesRestored || 0} tabelas e ${data.recordsRestored || 0} registros restaurados.`);
queryClient.invalidateQueries({ queryKey: ['backup-logs'] });
},
onError: (error: Error) => {
toast.error(`Erro ao restaurar backup: ${error.message}`);
},
onSettled: () => {
setIsRestoring(false);
}
});
return {
backupLogs,
logsLoading,
isBackingUp,
isRestoring,
createBackup: createBackup.mutate,
restoreBackup: restoreBackup.mutate
};
};
+151
View File
@@ -0,0 +1,151 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
interface BrandSettings {
company_name: string;
logo_url: string | null;
font_family: string;
}
export const useBrandSettings = () => {
const [brandSettings, setBrandSettings] = useState<BrandSettings>({
company_name: 'TrackSteel',
logo_url: null,
font_family: 'Arial'
});
const [isLoading, setIsLoading] = useState(true);
const loadBrandSettings = async () => {
try {
console.log('Loading brand settings...');
const { data, error } = await supabase
.from('brand_settings')
.select('*')
.limit(1)
.maybeSingle();
if (error) {
console.error('Error loading brand settings:', error);
return;
}
if (data) {
console.log('Brand settings loaded:', data);
setBrandSettings({
company_name: data.company_name || 'TrackSteel',
logo_url: data.logo_url,
font_family: data.font_family || 'Arial'
});
}
} catch (error) {
console.error('Error loading brand settings:', error);
} finally {
setIsLoading(false);
}
};
const saveBrandSettings = async (settings: BrandSettings) => {
try {
console.log('Saving brand settings:', settings);
// First, check if there's already a record
const { data: existingData } = await supabase
.from('brand_settings')
.select('id')
.limit(1)
.maybeSingle();
let result;
if (existingData) {
// Update existing record
result = await supabase
.from('brand_settings')
.update({
company_name: settings.company_name,
logo_url: settings.logo_url,
font_family: settings.font_family,
updated_at: new Date().toISOString()
})
.eq('id', existingData.id)
.select()
.single();
} else {
// Insert new record
result = await supabase
.from('brand_settings')
.insert({
company_name: settings.company_name,
logo_url: settings.logo_url,
font_family: settings.font_family
})
.select()
.single();
}
if (result.error) {
console.error('Error saving brand settings:', result.error);
toast.error('Erro ao salvar configurações: ' + result.error.message);
return { error: result.error };
}
console.log('Brand settings saved successfully:', result.data);
setBrandSettings(settings);
toast.success('Configurações salvas com sucesso!');
return { data: result.data };
} catch (error) {
console.error('Error saving brand settings:', error);
toast.error('Erro ao salvar configurações');
return { error };
}
};
const uploadLogo = async (file: File) => {
try {
console.log('Uploading logo file:', file.name);
// Generate unique filename
const fileExt = file.name.split('.').pop();
const fileName = `logo-${Date.now()}.${fileExt}`;
const { data, error } = await supabase.storage
.from('brand-assets')
.upload(fileName, file, {
cacheControl: '3600',
upsert: true
});
if (error) {
console.error('Error uploading logo:', error);
toast.error('Erro ao fazer upload do logo: ' + error.message);
return { error };
}
// Get public URL
const { data: { publicUrl } } = supabase.storage
.from('brand-assets')
.getPublicUrl(fileName);
console.log('Logo uploaded successfully. Public URL:', publicUrl);
return { data: { publicUrl, path: data.path } };
} catch (error) {
console.error('Error uploading logo:', error);
toast.error('Erro ao fazer upload do logo');
return { error };
}
};
useEffect(() => {
loadBrandSettings();
}, []);
return {
brandSettings,
setBrandSettings,
loadBrandSettings,
saveBrandSettings,
uploadLogo,
isLoading
};
};
+165
View File
@@ -0,0 +1,165 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
export interface Catalogo {
id: string;
titulo: string;
categoria: string;
disciplina: string;
palavras_chave: string[];
conteudo: string;
numero_paginas: number | null;
arquivo_urls?: string[];
created_at: string;
updated_at: string;
created_by: string | null;
}
export function useCatalogos() {
const [catalogos, setCatalogos] = useState<Catalogo[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [categoriaFilter, setCategoriaFilter] = useState('');
const [disciplinaFilter, setDisciplinaFilter] = useState('');
const fetchCatalogos = async () => {
try {
setLoading(true);
let query = supabase
.from('catalogos')
.select('*')
.order('created_at', { ascending: false });
// Aplicar filtros
if (searchTerm) {
query = query.or(`titulo.ilike.%${searchTerm}%,conteudo.ilike.%${searchTerm}%,disciplina.ilike.%${searchTerm}%`);
}
if (categoriaFilter && categoriaFilter !== 'all') {
query = query.eq('categoria', categoriaFilter);
}
if (disciplinaFilter && disciplinaFilter !== 'all') {
query = query.eq('disciplina', disciplinaFilter);
}
const { data, error } = await query;
if (error) throw error;
setCatalogos(data || []);
} catch (error) {
console.error('Erro ao buscar catálogos:', error);
toast.error('Erro ao carregar catálogos');
} finally {
setLoading(false);
}
};
const createCatalogo = async (catalogoData: Omit<Catalogo, 'id' | 'created_at' | 'updated_at' | 'created_by'>) => {
try {
const { data, error } = await supabase
.from('catalogos')
.insert([{
...catalogoData,
created_by: (await supabase.auth.getUser()).data.user?.id
}])
.select()
.single();
if (error) throw error;
setCatalogos(prev => [data, ...prev]);
toast.success('Catálogo criado com sucesso!');
return data;
} catch (error) {
console.error('Erro ao criar catálogo:', error);
toast.error('Erro ao criar catálogo');
throw error;
}
};
const updateCatalogo = async (id: string, catalogoData: Partial<Catalogo>) => {
try {
const { data, error } = await supabase
.from('catalogos')
.update(catalogoData)
.eq('id', id)
.select()
.single();
if (error) throw error;
setCatalogos(prev => prev.map(cat => cat.id === id ? data : cat));
toast.success('Catálogo atualizado com sucesso!');
return data;
} catch (error) {
console.error('Erro ao atualizar catálogo:', error);
toast.error('Erro ao atualizar catálogo');
throw error;
}
};
const deleteCatalogo = async (id: string) => {
try {
// Buscar o catálogo para obter os URLs dos arquivos
const { data: catalogo } = await supabase
.from('catalogos')
.select('arquivo_urls')
.eq('id', id)
.single();
// Deletar arquivos do storage se existirem
if (catalogo?.arquivo_urls && catalogo.arquivo_urls.length > 0) {
const fileNames = catalogo.arquivo_urls.map(url => {
const parts = url.split('/');
return parts[parts.length - 1];
});
await supabase.storage
.from('catalogo-documents')
.remove(fileNames);
}
const { error } = await supabase
.from('catalogos')
.delete()
.eq('id', id);
if (error) throw error;
setCatalogos(prev => prev.filter(cat => cat.id !== id));
toast.success('Catálogo excluído com sucesso!');
} catch (error) {
console.error('Erro ao excluir catálogo:', error);
toast.error('Erro ao excluir catálogo');
throw error;
}
};
const clearFilters = () => {
setSearchTerm('');
setCategoriaFilter('');
setDisciplinaFilter('');
};
useEffect(() => {
fetchCatalogos();
}, [searchTerm, categoriaFilter, disciplinaFilter]);
return {
catalogos,
loading,
searchTerm,
setSearchTerm,
categoriaFilter,
setCategoriaFilter,
disciplinaFilter,
setDisciplinaFilter,
createCatalogo,
updateCatalogo,
deleteCatalogo,
clearFilters
};
}
+138
View File
@@ -0,0 +1,138 @@
import { useState, useEffect, useMemo } from 'react';
import { useComponentesPeca } from '@/hooks/useComponentesPeca';
import { useDebounce } from '@/hooks/useDebounce';
export interface ComponenteAgrupado {
marca_componente: string;
descricao: string;
perfil: string;
peso_unitario: number;
quantidade_total: number;
peca_pai_info: string;
componente_ids: string[];
peca_ids: string[];
}
// Cache local para evitar buscas repetitivas
const componentesCache = new Map<string, ComponenteAgrupado[]>();
const CACHE_DURATION = 30000; // 30 segundos
export const useComponentesAgrupados = (ofNumber: string, fase: string, pecasData: any[] = []) => {
const [componentesAgrupados, setComponentesAgrupados] = useState<ComponenteAgrupado[]>([]);
const [loading, setLoading] = useState(false);
// Debounce para evitar chamadas em sequência rápida
const debouncedOfNumber = useDebounce(ofNumber, 300);
const debouncedFase = useDebounce(fase, 300);
// Buscar peças da OF/fase que têm componentes usando os dados passados como parâmetro
const pecasComComponentes = useMemo(() => {
if (!pecasData || !Array.isArray(pecasData) || !debouncedOfNumber || !debouncedFase) {
return [];
}
return pecasData.filter(peca =>
peca &&
peca.of_number === debouncedOfNumber &&
peca.etapa_fase === debouncedFase &&
peca.tem_componentes === true // Apenas peças com componentes
);
}, [pecasData, debouncedOfNumber, debouncedFase]);
// Gerar chave única para cache
const cacheKey = useMemo(() => {
if (!debouncedOfNumber || !debouncedFase || pecasComComponentes.length === 0) {
return '';
}
const pecaIds = pecasComComponentes.map(p => p.id).sort().join(',');
return `${debouncedOfNumber}_${debouncedFase}_${pecaIds}`;
}, [debouncedOfNumber, debouncedFase, pecasComComponentes]);
const pecaIds = pecasComComponentes.map(peca => peca.id);
// Só buscar componentes se há peças com componentes
const shouldFetchComponents = pecaIds.length > 0 && cacheKey;
const { componentes, loading: componentesLoading } = useComponentesPeca(
shouldFetchComponents ? pecaIds : []
);
// Verificar cache primeiro
useEffect(() => {
if (!cacheKey) {
setComponentesAgrupados([]);
setLoading(false);
return;
}
const cached = componentesCache.get(cacheKey);
if (cached && Date.now() - (cached as any).timestamp < CACHE_DURATION) {
console.log('📦 Cache HIT para componentes agrupados:', cacheKey);
setComponentesAgrupados(cached);
setLoading(false);
return;
}
setLoading(true);
}, [cacheKey]);
// Processar componentes quando disponíveis
useEffect(() => {
if (!cacheKey || componentesLoading || !componentes || componentes.length === 0) {
if (!componentesLoading && cacheKey && componentes.length === 0) {
// Não há componentes para essas peças, salvar cache vazio
const emptyResult: ComponenteAgrupado[] = [];
(emptyResult as any).timestamp = Date.now();
componentesCache.set(cacheKey, emptyResult);
setComponentesAgrupados(emptyResult);
setLoading(false);
}
return;
}
console.log('🔄 Processando componentes agrupados para cache:', cacheKey);
const grupos = new Map<string, ComponenteAgrupado>();
componentes.forEach(comp => {
const marca = comp.marca_componente;
const pecaPai = pecasComComponentes.find(p => p.id === comp.peca_id);
if (!pecaPai) return;
if (grupos.has(marca)) {
const grupo = grupos.get(marca)!;
grupo.quantidade_total += comp.quantidade_por_peca * (pecaPai.quantidade || 0);
grupo.peca_pai_info += `, ${pecaPai.marca}`;
grupo.componente_ids.push(comp.id);
grupo.peca_ids.push(comp.peca_id);
} else {
grupos.set(marca, {
marca_componente: marca,
descricao: comp.descricao || '',
perfil: comp.perfil || '',
peso_unitario: comp.peso_unitario,
quantidade_total: comp.quantidade_por_peca * (pecaPai.quantidade || 0),
peca_pai_info: pecaPai.marca,
componente_ids: [comp.id],
peca_ids: [comp.peca_id]
});
}
});
const resultado = Array.from(grupos.values());
// Salvar no cache
(resultado as any).timestamp = Date.now();
componentesCache.set(cacheKey, resultado);
setComponentesAgrupados(resultado);
setLoading(false);
}, [componentes, componentesLoading, pecasComComponentes, cacheKey]);
return {
componentesAgrupados,
loading: loading || componentesLoading
};
};
+208
View File
@@ -0,0 +1,208 @@
import { useState, useEffect, useRef } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
import { useAuth } from '@/hooks/useAuth';
export interface ComponentePeca {
id: string;
peca_id: string;
marca_componente: string;
descricao: string | null;
perfil: string | null;
peso_unitario: number;
quantidade_por_peca: number;
created_at: string;
updated_at: string;
}
export interface ComponenteFormData {
marca_componente: string;
descricao: string;
perfil: string;
peso_unitario: number;
quantidade_por_peca: number;
}
// Cache local para componentes
const componentesCache = new Map<string, ComponentePeca[]>();
const CACHE_DURATION = 30000; // 30 segundos
export function useComponentesPeca(pecaIds: string | string[] = []) {
const { user } = useAuth();
const [componentes, setComponentes] = useState<ComponentePeca[]>([]);
const [loading, setLoading] = useState(false);
const lastPecaIdsRef = useRef<string>('');
// Normalizar e validar pecaIds
const normalizedPecaIds = Array.isArray(pecaIds) ? pecaIds : (pecaIds ? [pecaIds] : []);
const validPecaIds = normalizedPecaIds.filter(id => id && id.trim() !== '');
const pecaIdsKey = validPecaIds.sort().join(',');
const loadComponentes = async () => {
// Evitar chamadas desnecessárias
if (validPecaIds.length === 0) {
setComponentes([]);
return;
}
// Evitar re-chamadas para os mesmos IDs
if (pecaIdsKey === lastPecaIdsRef.current) {
return;
}
// Verificar cache primeiro
const cached = componentesCache.get(pecaIdsKey);
if (cached && Date.now() - (cached as any).timestamp < CACHE_DURATION) {
console.log('📦 Cache HIT para componentes:', pecaIdsKey.substring(0, 50));
setComponentes(cached);
return;
}
try {
setLoading(true);
lastPecaIdsRef.current = pecaIdsKey;
console.log('🔄 Carregando componentes para peças:', validPecaIds.length);
const { data, error } = await supabase
.from('componentes_peca')
.select('*')
.in('peca_id', validPecaIds)
.order('marca_componente');
if (error) throw error;
const resultado = data || [];
// Salvar no cache
(resultado as any).timestamp = Date.now();
componentesCache.set(pecaIdsKey, resultado);
setComponentes(resultado);
console.log('✅ Componentes carregados:', resultado.length);
} catch (error) {
console.error('❌ Erro ao carregar componentes:', error);
// Não mostrar toast para erros de componentes, apenas log
// toast.error('Erro ao carregar componentes');
setComponentes([]);
} finally {
setLoading(false);
}
};
const fetchComponentesPeca = async (pecaId: string): Promise<ComponentePeca[]> => {
try {
const { data, error } = await supabase
.from('componentes_peca')
.select('*')
.eq('peca_id', pecaId)
.order('marca_componente');
if (error) throw error;
return data || [];
} catch (error) {
console.error('Erro ao buscar componentes da peça:', error);
toast.error('Erro ao buscar componentes da peça');
return [];
}
};
const saveComponente = async (formData: ComponenteFormData, pecaId: string) => {
if (!user || !pecaId) {
toast.error('Usuário não autenticado ou peça não selecionada');
return false;
}
try {
const { error } = await supabase
.from('componentes_peca')
.insert([{
...formData,
peca_id: pecaId,
user_id: user.id
}]);
if (error) throw error;
toast.success('Componente cadastrado com sucesso!');
// Invalidar cache
componentesCache.clear();
loadComponentes();
return true;
} catch (error) {
console.error('Erro ao salvar componente:', error);
toast.error('Erro ao salvar componente');
return false;
}
};
const updateComponente = async (id: string, formData: ComponenteFormData) => {
if (!user) {
toast.error('Usuário não autenticado');
return false;
}
try {
const { error } = await supabase
.from('componentes_peca')
.update(formData)
.eq('id', id);
if (error) throw error;
toast.success('Componente atualizado com sucesso!');
// Invalidar cache
componentesCache.clear();
loadComponentes();
return true;
} catch (error) {
console.error('Erro ao atualizar componente:', error);
toast.error('Erro ao atualizar componente');
return false;
}
};
const deleteComponente = async (id: string) => {
if (!user) {
toast.error('Usuário não autenticado');
return false;
}
try {
const { error } = await supabase
.from('componentes_peca')
.delete()
.eq('id', id);
if (error) throw error;
toast.success('Componente apagado com sucesso!');
// Invalidar cache
componentesCache.clear();
loadComponentes();
return true;
} catch (error) {
console.error('Erro ao apagar componente:', error);
toast.error('Erro ao apagar componente');
return false;
}
};
useEffect(() => {
loadComponentes();
}, [pecaIdsKey]); // Usar a chave em vez do array diretamente
return {
componentes,
loading,
saveComponente,
updateComponente,
deleteComponente,
loadComponentes,
fetchComponentesPeca
};
}
+148
View File
@@ -0,0 +1,148 @@
import { useState } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
import { ComponentePeca } from './useComponentesPeca';
export function useComponentesTableActions() {
const [importLoading, setImportLoading] = useState(false);
const exportTemplateCSV = () => {
const headers = [
'marca_componente',
'descricao',
'perfil',
'peso_unitario',
'quantidade_por_peca'
];
const csvContent = headers.join(',');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'template_componentes.csv';
link.click();
toast.success('Template CSV de componentes baixado com sucesso!');
};
const exportComponentesCSV = (componentes: ComponentePeca[], pecaMarca: string) => {
if (componentes.length === 0) {
toast.error('Nenhum componente para exportar');
return;
}
const headers = [
'marca_componente',
'descricao',
'perfil',
'peso_unitario',
'quantidade_por_peca'
];
const csvData = componentes.map(comp => [
comp.marca_componente,
comp.descricao || '',
comp.perfil || '',
comp.peso_unitario,
comp.quantidade_por_peca
]);
const csvContent = [
headers.join(','),
...csvData.map(row => row.map(field => `"${field}"`).join(','))
].join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `componentes_${pecaMarca}_${new Date().toISOString().split('T')[0]}.csv`;
link.click();
toast.success(`${componentes.length} componentes exportados com sucesso!`);
};
const importComponentesCSV = async (file: File, pecaId: string, userId: string, onSuccess: () => void) => {
if (!file || !file.name.endsWith('.csv')) {
toast.error('Selecione um arquivo CSV válido');
return;
}
setImportLoading(true);
try {
const text = await file.text();
const lines = text.split('\n').filter(line => line.trim());
if (lines.length < 2) {
toast.error('Arquivo CSV deve conter pelo menos uma linha de dados');
return;
}
const headers = lines[0].split(',').map(h => h.trim().replace(/"/g, ''));
const expectedHeaders = ['marca_componente', 'descricao', 'perfil', 'peso_unitario', 'quantidade_por_peca'];
const hasRequiredHeaders = expectedHeaders.every(header => headers.includes(header));
if (!hasRequiredHeaders) {
toast.error('Arquivo CSV deve conter as colunas: ' + expectedHeaders.join(', '));
return;
}
const componentes = [];
for (let i = 1; i < lines.length; i++) {
const values = lines[i].split(',').map(v => v.trim().replace(/"/g, ''));
if (values.length !== headers.length) continue;
const componenteData: any = {
peca_id: pecaId,
user_id: userId
};
headers.forEach((header, index) => {
let value = values[index];
if (header === 'peso_unitario') {
componenteData[header] = parseFloat(value) || 0;
} else if (header === 'quantidade_por_peca') {
componenteData[header] = parseInt(value) || 1;
} else {
componenteData[header] = value || null;
}
});
if (!componenteData.marca_componente) continue;
componentes.push(componenteData);
}
if (componentes.length === 0) {
toast.error('Nenhum componente válido encontrado no arquivo');
return;
}
const { error } = await supabase
.from('componentes_peca')
.insert(componentes);
if (error) throw error;
toast.success(`${componentes.length} componente(s) importado(s) com sucesso!`);
onSuccess();
} catch (error) {
console.error('Erro ao importar componentes:', error);
toast.error('Erro ao importar componentes do CSV');
} finally {
setImportLoading(false);
}
};
return {
importLoading,
exportTemplateCSV,
exportComponentesCSV,
importComponentesCSV
};
}
+249
View File
@@ -0,0 +1,249 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
import { useAuth } from './useAuth';
import { CronogramaOf } from '@/types/cronograma';
export const useCronogramaOperations = () => {
const { user } = useAuth();
const [cronogramas, setCronogramas] = useState<CronogramaOf[]>([]);
const [loading, setLoading] = useState(false);
const loadCronogramas = async () => {
try {
setLoading(true);
const { data: cronogramasData, error: cronogramasError } = await supabase
.from('cronogramas_of')
.select(`
*,
ordens_fabricacao (
num_of,
descritivo
),
profiles!cronogramas_of_gestor_id_fkey (
full_name,
email
)
`)
.order('created_at', { ascending: false });
if (cronogramasError) throw cronogramasError;
// Carregar processos para cada cronograma
const cronogramasComProcessos = await Promise.all(
(cronogramasData || []).map(async (cronograma) => {
const { data: processos, error: processosError } = await supabase
.from('processos_cronograma')
.select('*')
.eq('cronograma_id', cronograma.id)
.order('ordem');
if (processosError) throw processosError;
return {
id: cronograma.id,
of_id: cronograma.of_id,
gestor_id: cronograma.gestor_id,
revisao: cronograma.revisao,
processos: processos || [],
ordem_fabricacao: cronograma.ordens_fabricacao,
gestor_profile: cronograma.profiles
} as CronogramaOf;
})
);
setCronogramas(cronogramasComProcessos);
} catch (error) {
console.error('Erro ao carregar cronogramas:', error);
toast.error('Erro ao carregar cronogramas');
} finally {
setLoading(false);
}
};
const saveCronograma = async (cronograma: Partial<CronogramaOf> & { processos: any[] }) => {
try {
if (!user) throw new Error('Usuário não autenticado');
let cronogramaId: string;
if (cronograma.id) {
// Atualizar cronograma existente
const novaRevisao = cronograma.revisao ? cronograma.revisao + 1 : 1;
const { data: updatedCronograma, error: updateError } = await supabase
.from('cronogramas_of')
.update({
gestor_id: cronograma.gestor_id,
revisao: novaRevisao,
updated_at: new Date().toISOString()
})
.eq('id', cronograma.id)
.select()
.single();
if (updateError) throw updateError;
cronogramaId = updatedCronograma.id;
// Deletar processos antigos
const { error: deleteError } = await supabase
.from('processos_cronograma')
.delete()
.eq('cronograma_id', cronogramaId);
if (deleteError) throw deleteError;
} else {
// Verificar se já existe cronograma para esta OF
const { data: existingCronograma } = await supabase
.from('cronogramas_of')
.select('id, revisao')
.eq('of_id', cronograma.of_id)
.single();
if (existingCronograma) {
// Atualizar cronograma existente
const novaRevisao = existingCronograma.revisao + 1;
const { data: updatedCronograma, error: updateError } = await supabase
.from('cronogramas_of')
.update({
gestor_id: cronograma.gestor_id,
revisao: novaRevisao,
updated_at: new Date().toISOString()
})
.eq('id', existingCronograma.id)
.select()
.single();
if (updateError) throw updateError;
cronogramaId = updatedCronograma.id;
// Deletar processos antigos
const { error: deleteError } = await supabase
.from('processos_cronograma')
.delete()
.eq('cronograma_id', cronogramaId);
if (deleteError) throw deleteError;
} else {
// Criar novo cronograma
const { data: newCronograma, error: insertError } = await supabase
.from('cronogramas_of')
.insert({
of_id: cronograma.of_id,
gestor_id: cronograma.gestor_id,
revisao: 1,
created_by: user.id
})
.select()
.single();
if (insertError) throw insertError;
cronogramaId = newCronograma.id;
}
}
// Inserir novos processos
const processosParaInserir = cronograma.processos.map(processo => ({
cronograma_id: cronogramaId,
nome_processo: processo.nome_processo,
data_inicio: processo.data_inicio,
data_fim: processo.data_fim,
ordem: processo.ordem
}));
const { error: processosError } = await supabase
.from('processos_cronograma')
.insert(processosParaInserir);
if (processosError) throw processosError;
toast.success('Cronograma salvo com sucesso!');
loadCronogramas();
return true;
} catch (error) {
console.error('Erro ao salvar cronograma:', error);
toast.error('Erro ao salvar cronograma');
return false;
}
};
const deleteCronograma = async (cronogramaId: string) => {
try {
const { error } = await supabase
.from('cronogramas_of')
.delete()
.eq('id', cronogramaId);
if (error) throw error;
toast.success('Cronograma removido com sucesso!');
loadCronogramas();
return true;
} catch (error) {
console.error('Erro ao deletar cronograma:', error);
toast.error('Erro ao remover cronograma');
return false;
}
};
const getCronogramaPorOf = async (ofId: string) => {
try {
const { data: cronograma, error } = await supabase
.from('cronogramas_of')
.select(`
*,
ordens_fabricacao (
num_of,
descritivo
),
profiles!cronogramas_of_gestor_id_fkey (
full_name,
email
)
`)
.eq('of_id', ofId)
.single();
if (error && error.code !== 'PGRST116') throw error;
if (!cronograma) return null;
const { data: processos, error: processosError } = await supabase
.from('processos_cronograma')
.select('*')
.eq('cronograma_id', cronograma.id)
.order('ordem');
if (processosError) throw processosError;
return {
id: cronograma.id,
of_id: cronograma.of_id,
gestor_id: cronograma.gestor_id,
revisao: cronograma.revisao,
processos: processos || [],
ordem_fabricacao: cronograma.ordens_fabricacao,
gestor_profile: cronograma.profiles
} as CronogramaOf;
} catch (error) {
console.error('Erro ao buscar cronograma:', error);
return null;
}
};
useEffect(() => {
loadCronogramas();
}, []);
return {
cronogramas,
loading,
loadCronogramas,
saveCronograma,
deleteCronograma,
getCronogramaPorOf
};
};
+251
View File
@@ -0,0 +1,251 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
import { useAuth } from './useAuth';
export interface ProcessoCronograma {
id?: string;
nome_processo: string;
data_inicio: string;
data_fim: string;
ordem: number;
}
export interface CronogramaOf {
id?: string;
of_id: string;
gestor_id: string;
revisao: number;
processos: ProcessoCronograma[];
peso_total?: number;
ordem_fabricacao?: {
num_of: string;
descritivo: string;
peso_total?: number;
};
gestor_profile?: {
full_name: string;
email: string;
};
}
export const useCronogramas = () => {
const { user } = useAuth();
const [cronogramas, setCronogramas] = useState<CronogramaOf[]>([]);
const [loading, setLoading] = useState(false);
const loadCronogramas = async () => {
try {
setLoading(true);
const { data: cronogramasData, error: cronogramasError } = await supabase
.from('cronogramas_of')
.select(`
*,
ordens_fabricacao (
num_of,
descritivo,
peso_total
),
profiles!gestor_id (
full_name,
email
)
`)
.order('created_at', { ascending: false });
if (cronogramasError) throw cronogramasError;
// Carregar processos para cada cronograma
const cronogramasComProcessos = await Promise.all(
(cronogramasData || []).map(async (cronograma) => {
const { data: processos, error: processosError } = await supabase
.from('processos_cronograma')
.select('*')
.eq('cronograma_id', cronograma.id)
.order('ordem');
if (processosError) throw processosError;
return {
id: cronograma.id,
of_id: cronograma.of_id,
gestor_id: cronograma.gestor_id,
revisao: cronograma.revisao,
processos: processos || [],
peso_total: cronograma.ordens_fabricacao?.peso_total,
ordem_fabricacao: cronograma.ordens_fabricacao,
gestor_profile: cronograma.profiles
} as CronogramaOf;
})
);
setCronogramas(cronogramasComProcessos);
} catch (error) {
console.error('Erro ao carregar cronogramas:', error);
toast.error('Erro ao carregar cronogramas');
} finally {
setLoading(false);
}
};
const saveCronograma = async (cronograma: Omit<CronogramaOf, 'id'>) => {
try {
if (!user) throw new Error('Usuário não autenticado');
// Verificar se já existe cronograma para esta OF
const { data: existingCronograma } = await supabase
.from('cronogramas_of')
.select('id, revisao')
.eq('of_id', cronograma.of_id)
.single();
let cronogramaId: string;
let novaRevisao = 1;
if (existingCronograma) {
// Atualizar cronograma existente
novaRevisao = existingCronograma.revisao + 1;
const { data: updatedCronograma, error: updateError } = await supabase
.from('cronogramas_of')
.update({
gestor_id: cronograma.gestor_id,
revisao: novaRevisao,
updated_at: new Date().toISOString()
})
.eq('id', existingCronograma.id)
.select()
.single();
if (updateError) throw updateError;
cronogramaId = updatedCronograma.id;
// Deletar processos antigos
const { error: deleteError } = await supabase
.from('processos_cronograma')
.delete()
.eq('cronograma_id', cronogramaId);
if (deleteError) throw deleteError;
} else {
// Criar novo cronograma
const { data: newCronograma, error: insertError } = await supabase
.from('cronogramas_of')
.insert({
of_id: cronograma.of_id,
gestor_id: cronograma.gestor_id,
revisao: 1,
created_by: user.id
})
.select()
.single();
if (insertError) throw insertError;
cronogramaId = newCronograma.id;
}
// Inserir novos processos
const processosParaInserir = cronograma.processos.map(processo => ({
cronograma_id: cronogramaId,
nome_processo: processo.nome_processo,
data_inicio: processo.data_inicio,
data_fim: processo.data_fim,
ordem: processo.ordem
}));
const { error: processosError } = await supabase
.from('processos_cronograma')
.insert(processosParaInserir);
if (processosError) throw processosError;
toast.success('Cronograma salvo com sucesso!');
loadCronogramas();
return true;
} catch (error) {
console.error('Erro ao salvar cronograma:', error);
toast.error('Erro ao salvar cronograma');
return false;
}
};
const deleteCronograma = async (cronogramaId: string) => {
try {
const { error } = await supabase
.from('cronogramas_of')
.delete()
.eq('id', cronogramaId);
if (error) throw error;
toast.success('Cronograma removido com sucesso!');
loadCronogramas();
return true;
} catch (error) {
console.error('Erro ao deletar cronograma:', error);
toast.error('Erro ao remover cronograma');
return false;
}
};
const getCronogramaPorOf = async (ofId: string) => {
try {
const { data: cronograma, error } = await supabase
.from('cronogramas_of')
.select(`
*,
ordens_fabricacao (
num_of,
descritivo,
peso_total
),
profiles!gestor_id (
full_name,
email
)
`)
.eq('of_id', ofId)
.single();
if (error && error.code !== 'PGRST116') throw error;
if (!cronograma) return null;
const { data: processos, error: processosError } = await supabase
.from('processos_cronograma')
.select('*')
.eq('cronograma_id', cronograma.id)
.order('ordem');
if (processosError) throw processosError;
return {
id: cronograma.id,
of_id: cronograma.of_id,
gestor_id: cronograma.gestor_id,
revisao: cronograma.revisao,
processos: processos || [],
peso_total: cronograma.ordens_fabricacao?.peso_total,
ordem_fabricacao: cronograma.ordens_fabricacao,
gestor_profile: cronograma.profiles
} as CronogramaOf;
} catch (error) {
console.error('Erro ao buscar cronograma:', error);
return null;
}
};
useEffect(() => {
loadCronogramas();
}, []);
return {
cronogramas,
loading,
loadCronogramas,
saveCronograma,
deleteCronograma,
getCronogramaPorOf
};
};
+182
View File
@@ -0,0 +1,182 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
export interface DashboardProcesso {
id: string;
nome: string;
pesoTotal: number;
pesoFabricado: number;
progressoReal: number;
progressoEsperado: number;
status: 'verde' | 'amarelo' | 'vermelho';
dataInicioPlanned?: string;
dataFimPlanned?: string;
dataInicioReal?: string;
dataFimReal?: string;
dadosGrafico: {
data: string;
planejado: number;
realizado: number;
}[];
}
export interface DashboardData {
of: string;
progressoGeral: number;
pesoTotalFabricado: number;
tonelagem: number;
processos: DashboardProcesso[];
}
export const useDashboardProducao = (ofNumber: string) => {
const [dashboardData, setDashboardData] = useState<DashboardData | null>(null);
const [loading, setLoading] = useState(false);
const fetchDashboardData = async () => {
if (!ofNumber) {
setDashboardData(null);
return;
}
setLoading(true);
try {
// Buscar dados da OF
const { data: ofData, error: ofError } = await supabase
.from('ordens_fabricacao')
.select('*')
.eq('num_of', ofNumber)
.single();
if (ofError) throw ofError;
// Buscar peças da OF
const { data: pecasData, error: pecasError } = await supabase
.from('pecas')
.select('*')
.eq('of_number', ofNumber);
if (pecasError) throw pecasError;
// Buscar apontamentos da OF
const { data: apontamentosData, error: apontamentosError } = await supabase
.from('apontamentos_producao')
.select(`
*,
peca:pecas(peso_unitario),
processo:processos_fabricacao(nome, ordem)
`)
.eq('of_number', ofNumber);
if (apontamentosError) throw apontamentosError;
// Buscar processos
const { data: processosData, error: processosError } = await supabase
.from('processos_fabricacao')
.select('*')
.eq('ativo', true)
.order('ordem');
if (processosError) throw processosError;
// Calcular dados do dashboard
const pesoTotalPlanejado = pecasData.reduce((total, peca) =>
total + (peca.quantidade * (peca.peso_unitario || 0)), 0
);
const pesoTotalFabricado = apontamentosData.reduce((total, apontamento) =>
total + (apontamento.quantidade_produzida * (apontamento.peca?.peso_unitario || 0)), 0
);
const progressoGeral = pesoTotalPlanejado > 0 ? (pesoTotalFabricado / pesoTotalPlanejado) * 100 : 0;
// Processar dados por processo
const processos: DashboardProcesso[] = processosData.map(processo => {
const apontamentosProcesso = apontamentosData.filter(a => a.processo_id === processo.id);
const pesoFabricadoProcesso = apontamentosProcesso.reduce((total, a) =>
total + (a.quantidade_produzida * (a.peca?.peso_unitario || 0)), 0
);
const progressoReal = pesoTotalPlanejado > 0 ? (pesoFabricadoProcesso / pesoTotalPlanejado) * 100 : 0;
// Calcular progresso esperado baseado na data atual
const hoje = new Date();
const dataInicio = ofData.data_abertura ? new Date(ofData.data_abertura) : new Date();
const dataFim = ofData.data_prazo ? new Date(ofData.data_prazo) : new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
const diasTotais = Math.max(1, Math.ceil((dataFim.getTime() - dataInicio.getTime()) / (1000 * 60 * 60 * 24)));
const diasDecorridos = Math.max(0, Math.ceil((hoje.getTime() - dataInicio.getTime()) / (1000 * 60 * 60 * 24)));
const progressoEsperado = Math.min(100, (diasDecorridos / diasTotais) * 100);
// Determinar status
let status: 'verde' | 'amarelo' | 'vermelho' = 'verde';
if (progressoReal < progressoEsperado * 0.8) {
status = 'vermelho';
} else if (progressoReal < progressoEsperado * 0.9) {
status = 'amarelo';
}
// Gerar dados do gráfico (últimos 30 dias)
const dadosGrafico = [];
const dataAtual = new Date();
for (let i = 29; i >= 0; i--) {
const data = new Date(dataAtual);
data.setDate(data.getDate() - i);
const dataStr = data.toISOString().split('T')[0];
// Calcular progresso planejado para esta data
const diasDesdeInicio = Math.max(0, Math.ceil((data.getTime() - dataInicio.getTime()) / (1000 * 60 * 60 * 24)));
const planejado = Math.min(pesoTotalPlanejado, (diasDesdeInicio / diasTotais) * pesoTotalPlanejado);
// Calcular progresso real até esta data
const apontamentosAteData = apontamentosProcesso.filter(a => a.data_apontamento <= dataStr);
const realizado = apontamentosAteData.reduce((total, a) =>
total + (a.quantidade_produzida * (a.peca?.peso_unitario || 0)), 0
);
dadosGrafico.push({
data: dataStr,
planejado,
realizado
});
}
return {
id: processo.id,
nome: processo.nome,
pesoTotal: pesoTotalPlanejado,
pesoFabricado: pesoFabricadoProcesso,
progressoReal,
progressoEsperado,
status,
dadosGrafico
};
});
setDashboardData({
of: ofNumber,
progressoGeral,
pesoTotalFabricado,
tonelagem: pesoTotalPlanejado,
processos
});
} catch (error) {
console.error('Erro ao buscar dados do dashboard:', error);
toast.error('Erro ao carregar dados do dashboard');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchDashboardData();
}, [ofNumber]);
return {
dashboardData,
loading,
refetch: fetchDashboardData
};
};
+538
View File
@@ -0,0 +1,538 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
export interface ProcessoConsolidado {
processo_id: string;
processo_nome: string;
processo_cor: string;
data_apontamento: string;
peso_acumulado: number;
}
export interface DateRange {
data_inicio_grafico: string;
data_fim_grafico: string;
}
export interface DashboardProcesso {
id: string;
nome: string;
pesoTotal: number;
pesoFabricado: number;
progressoReal: number;
progressoEsperado: number;
status: 'verde' | 'amarelo' | 'vermelho' | 'azul';
dadosGrafico: {
data: string;
planejado: number;
realizado: number;
}[];
}
export interface DashboardDataOtimizado {
of: string;
progressoGeral: number;
pesoTotalFabricado: number;
tonelagem: number;
processos: DashboardProcesso[];
dadosConsolidados: ProcessoConsolidado[];
dateRange: DateRange;
}
export const useDashboardProducaoOtimizado = (ofNumber: string) => {
const [dashboardData, setDashboardData] = useState<DashboardDataOtimizado | null>(null);
const [loading, setLoading] = useState(false);
const fetchDashboardData = async () => {
if (!ofNumber) {
setDashboardData(null);
return;
}
setLoading(true);
try {
console.log('Buscando dados do dashboard para OF:', ofNumber);
// 1. Buscar dados da OF para obter o peso total planejado
const { data: ofData, error: ofError } = await supabase
.from('ordens_fabricacao')
.select('*')
.eq('num_of', ofNumber)
.single();
if (ofError) {
console.error('Erro ao buscar OF:', ofError);
throw ofError;
}
console.log('Dados da OF encontrados:', ofData);
// 2. Buscar ficha técnica para obter peso total se não estiver na OF
let pesoTotalPlanejado = ofData.peso_total || 0;
if (!pesoTotalPlanejado) {
const { data: fichaTecnica, error: fichaError } = await supabase
.from('ficha_tecnica_contratos')
.select('quantidade')
.eq('of_number', ofNumber)
.maybeSingle();
if (!fichaError && fichaTecnica?.quantidade) {
pesoTotalPlanejado = fichaTecnica.quantidade;
}
}
// 3. Se ainda não temos peso, calcular das peças cadastradas
if (!pesoTotalPlanejado) {
const { data: pecasData, error: pecasError } = await supabase
.from('pecas')
.select('peso_unitario, quantidade')
.eq('of_number', ofNumber);
if (pecasError) {
console.error('Erro ao buscar peças:', pecasError);
throw pecasError;
}
pesoTotalPlanejado = pecasData.reduce((total, peca) => {
const pesoUnitario = peca.peso_unitario || 0;
const quantidade = peca.quantidade || 0;
return total + (pesoUnitario * quantidade);
}, 0);
}
console.log('Peso total planejado (BASE DE TODOS OS CÁLCULOS):', pesoTotalPlanejado, 'kg');
// 4. Buscar apontamentos da OF com joins corretos
const { data: apontamentosData, error: apontamentosError } = await supabase
.from('apontamentos_producao')
.select(`
*,
peca:pecas(peso_unitario, marca),
processo:processos_fabricacao(nome, ordem, cor),
componente:componentes_peca(peso_unitario, marca_componente)
`)
.eq('of_number', ofNumber);
if (apontamentosError) {
console.error('Erro ao buscar apontamentos:', apontamentosError);
throw apontamentosError;
}
console.log('Apontamentos encontrados:', apontamentosData);
// 5. Calcular peso total fabricado - apenas processo de solda
let pesoTotalFabricado = 0;
// Buscar ID do processo de solda
const processoSolda = await supabase
.from('processos_fabricacao')
.select('id')
.ilike('nome', '%solda%')
.single();
if (!processoSolda.error && processoSolda.data) {
const apontamentosSolda = apontamentosData.filter(a => a.processo_id === processoSolda.data.id);
apontamentosSolda.forEach(apontamento => {
let pesoUnitario = 0;
if (apontamento.tipo_apontamento === 'componente' && apontamento.componente?.peso_unitario) {
pesoUnitario = Number(apontamento.componente.peso_unitario);
} else if (apontamento.tipo_apontamento === 'peca' && apontamento.peca?.peso_unitario) {
pesoUnitario = Number(apontamento.peca.peso_unitario);
}
const pesoApontamento = pesoUnitario * Number(apontamento.quantidade_produzida);
pesoTotalFabricado += pesoApontamento;
console.log(`Apontamento Solda ID: ${apontamento.id}, Tipo: ${apontamento.tipo_apontamento}, Peso unitário: ${pesoUnitario} kg, Quantidade: ${apontamento.quantidade_produzida}, Peso total: ${pesoApontamento} kg`);
});
}
console.log('Peso total fabricado (apenas solda):', pesoTotalFabricado, 'kg');
// 6. Calcular progresso geral - será recalculado após processar todos os processos
let progressoGeral = 0;
console.log(`PROGRESSO GERAL: ${pesoTotalFabricado} kg / ${pesoTotalPlanejado} kg = ${progressoGeral.toFixed(2)}%`);
// 7. Buscar range de datas
const { data: dateRangeData, error: dateRangeError } = await supabase
.rpc('get_dashboard_date_range', { of_number_param: ofNumber });
if (dateRangeError) {
console.error('Erro ao buscar range de datas:', dateRangeError);
}
// 8. Buscar dados consolidados
const { data: consolidatedData, error: consolidatedError } = await supabase
.rpc('get_dashboard_consolidated_data', { of_number_param: ofNumber });
if (consolidatedError) {
console.error('Erro ao buscar dados consolidados:', consolidatedError);
}
// 9. Buscar processos
const { data: processosData, error: processosError } = await supabase
.from('processos_fabricacao')
.select('*')
.eq('ativo', true)
.order('ordem');
if (processosError) {
console.error('Erro ao buscar processos:', processosError);
throw processosError;
}
// 10. Buscar cronograma da OF para calcular progresso esperado correto
const { data: cronogramaOf } = await supabase
.from('cronogramas_of')
.select(`
id,
processos_cronograma:processos_cronograma(
nome_processo,
data_inicio,
data_fim
)
`)
.eq('of_id', ofData.id)
.maybeSingle();
console.log('Cronograma da OF encontrado:', cronogramaOf);
// Função para obter percentual esperado baseado nas datas do cronograma
const calcularProgressoEsperado = (nomeProcesso: string): number => {
if (!cronogramaOf?.processos_cronograma) return 0;
const hoje = new Date();
let processoCronograma = null;
// Mapear nomes dos processos para os do cronograma
if (nomeProcesso.toLowerCase().includes('corte') || nomeProcesso.toLowerCase().includes('solda')) {
// Usar datas do processo "Fabricação"
processoCronograma = cronogramaOf.processos_cronograma.find(p =>
p.nome_processo.toLowerCase().includes('fabricação') ||
p.nome_processo.toLowerCase().includes('fabricacao')
);
} else if (nomeProcesso.toLowerCase().includes('pint') || nomeProcesso.toLowerCase().includes('galv') ||
nomeProcesso.toLowerCase().includes('expedição') || nomeProcesso.toLowerCase().includes('expedicao') ||
nomeProcesso.toLowerCase().includes('montagem')) {
// Usar datas do processo "Instalação" (incluindo "montagem")
processoCronograma = cronogramaOf.processos_cronograma.find(p =>
p.nome_processo.toLowerCase().includes('instalação') ||
p.nome_processo.toLowerCase().includes('instalacao')
);
} else {
// Buscar processo com nome exato ou similar
processoCronograma = cronogramaOf.processos_cronograma.find(p =>
p.nome_processo.toLowerCase().includes(nomeProcesso.toLowerCase()) ||
nomeProcesso.toLowerCase().includes(p.nome_processo.toLowerCase())
);
}
if (!processoCronograma) {
console.log(`Processo ${nomeProcesso} não encontrado no cronograma`);
return 0;
}
const dataInicio = new Date(processoCronograma.data_inicio + 'T00:00:00');
const dataFim = new Date(processoCronograma.data_fim + 'T23:59:59');
console.log(`Processo ${nomeProcesso} - Inicio: ${dataInicio.toDateString()}, Fim: ${dataFim.toDateString()}, Hoje: ${hoje.toDateString()}`);
// Se ainda não chegou na data de início, progresso esperado é 0%
if (hoje < dataInicio) {
console.log(`Data atual (${hoje.toDateString()}) é menor que data de início (${dataInicio.toDateString()}) - Progresso esperado: 0%`);
return 0;
}
const MS_PER_DAY = 1000 * 60 * 60 * 24;
const diasTotais = Math.max(1, Math.floor((dataFim.getTime() - dataInicio.getTime()) / MS_PER_DAY) + 1);
const diasDecorridos = Math.floor((hoje.getTime() - dataInicio.getTime()) / MS_PER_DAY) + 1;
// Calcular percentual baseado nos dias
let percentualEsperado = (diasDecorridos / diasTotais) * 100;
// Se passou da data fim, manter em 100%
if (hoje > dataFim) {
percentualEsperado = 100;
console.log(`Data atual passou da data fim - Progresso esperado mantido em: 100%`);
} else {
console.log(`Processo ${nomeProcesso}: ${diasDecorridos}/${diasTotais} dias = ${percentualEsperado.toFixed(2)}%`);
}
return Math.min(100, Math.max(0, percentualEsperado));
};
// 11. Processar dados por processo individual
const processos: DashboardProcesso[] = await Promise.all(processosData.map(async (processo) => {
const apontamentosProcesso = apontamentosData.filter(a => a.processo_id === processo.id);
let pesoFabricadoProcesso = 0;
apontamentosProcesso.forEach(a => {
let pesoUnitario = 0;
if (a.tipo_apontamento === 'componente' && a.componente?.peso_unitario) {
pesoUnitario = Number(a.componente.peso_unitario);
} else if (a.tipo_apontamento === 'peca' && a.peca?.peso_unitario) {
pesoUnitario = Number(a.peca.peso_unitario);
}
pesoFabricadoProcesso += pesoUnitario * Number(a.quantidade_produzida);
});
const progressoReal = pesoTotalPlanejado > 0 ? (pesoFabricadoProcesso / pesoTotalPlanejado) * 100 : 0;
console.log(`PROCESSO ${processo.nome}: ${pesoFabricadoProcesso} kg fabricado / ${pesoTotalPlanejado} kg planejado = ${progressoReal.toFixed(2)}%`);
// Calcular progresso esperado usando as datas do cronograma
const progressoEsperado = calcularProgressoEsperado(processo.nome);
// Determinar status baseado na comparação entre progresso real e esperado
let status: 'verde' | 'amarelo' | 'vermelho' | 'azul' = 'verde';
// Se o progresso esperado chegou a 100% (passou da data fim), usar cor vermelha na barra
if (progressoEsperado >= 100) {
if (progressoReal >= 100) {
status = 'verde'; // Concluído no prazo
} else {
status = 'vermelho'; // Atrasado (passou da data fim mas não concluído)
}
} else if (progressoEsperado > 0) {
// Comparar progresso real com esperado
if (progressoReal > progressoEsperado) {
status = 'azul'; // Adiantado
} else {
const percentualDoEsperado = (progressoReal / progressoEsperado) * 100;
if (percentualDoEsperado < 60) {
status = 'vermelho'; // Muito atrasado
} else if (percentualDoEsperado < 80) {
status = 'amarelo'; // Atenção
} else {
status = 'verde'; // No prazo
}
}
} else {
// Se não há progresso esperado ainda (antes da data de início), usar verde
status = 'verde';
}
console.log(`STATUS ${processo.nome}: ${progressoReal.toFixed(2)}% real vs ${progressoEsperado.toFixed(2)}% esperado -> ${status}`);
// Gerar dados do gráfico usando as datas específicas do cronograma do processo
const dadosGrafico = [];
// Buscar as datas específicas deste processo no cronograma
let dataInicioProcesso: Date | null = null;
let dataFimProcesso: Date | null = null;
if (cronogramaOf?.processos_cronograma) {
let processoCronograma = null;
// Mapear nomes dos processos para os do cronograma
if (processo.nome.toLowerCase().includes('corte') || processo.nome.toLowerCase().includes('solda')) {
processoCronograma = cronogramaOf.processos_cronograma.find(p =>
p.nome_processo.toLowerCase().includes('fabricação') ||
p.nome_processo.toLowerCase().includes('fabricacao')
);
} else if (processo.nome.toLowerCase().includes('pint') || processo.nome.toLowerCase().includes('galv') ||
processo.nome.toLowerCase().includes('expedição') || processo.nome.toLowerCase().includes('expedicao') ||
processo.nome.toLowerCase().includes('montagem')) {
processoCronograma = cronogramaOf.processos_cronograma.find(p =>
p.nome_processo.toLowerCase().includes('instalação') ||
p.nome_processo.toLowerCase().includes('instalacao')
);
} else {
processoCronograma = cronogramaOf.processos_cronograma.find(p =>
p.nome_processo.toLowerCase().includes(processo.nome.toLowerCase()) ||
processo.nome.toLowerCase().includes(p.nome_processo.toLowerCase())
);
}
if (processoCronograma) {
dataInicioProcesso = new Date(processoCronograma.data_inicio + 'T00:00:00');
dataFimProcesso = new Date(processoCronograma.data_fim + 'T23:59:59');
}
}
// Se não há cronograma específico, usar as datas da OF como fallback
if (!dataInicioProcesso || !dataFimProcesso) {
dataInicioProcesso = ofData.data_abertura ? new Date(ofData.data_abertura + 'T00:00:00') : new Date();
dataFimProcesso = ofData.data_prazo ? new Date(ofData.data_prazo + 'T23:59:59') : new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
}
// Verificar se há apontamentos fora do período planejado e ajustar o range se necessário
let dataInicioGrafico = dataInicioProcesso;
let dataFimGrafico = dataFimProcesso;
if (apontamentosProcesso.length > 0) {
const datasMaisAntigas = apontamentosProcesso.map(a => new Date(a.data_apontamento + 'T00:00:00'));
const datasMaisRecentes = apontamentosProcesso.map(a => new Date(a.data_apontamento + 'T23:59:59'));
const menorDataApontamento = new Date(Math.min(...datasMaisAntigas.map(d => d.getTime())));
const maiorDataApontamento = new Date(Math.max(...datasMaisRecentes.map(d => d.getTime())));
// Estender o período se houver apontamentos fora do planejado
if (menorDataApontamento < dataInicioGrafico) {
dataInicioGrafico = menorDataApontamento;
}
if (maiorDataApontamento > dataFimGrafico) {
dataFimGrafico = maiorDataApontamento;
}
}
const MS_PER_DAY = 1000 * 60 * 60 * 24;
const diasTotaisProcesso = Math.max(1, Math.floor((dataFimProcesso.getTime() - dataInicioProcesso.getTime()) / MS_PER_DAY) + 1);
const diasRange = Math.floor((dataFimGrafico.getTime() - dataInicioGrafico.getTime()) / MS_PER_DAY);
for (let i = 0; i <= diasRange; i++) {
const data = new Date(dataInicioGrafico);
data.setDate(data.getDate() + i);
const dataStr = data.toISOString().split('T')[0];
// Calcular progresso planejado baseado no cronograma específico do processo (percentual dos dias)
let planejado = 0;
if (data >= dataInicioProcesso && data <= dataFimProcesso) {
const diasDecorridos = Math.floor((Math.min(data.getTime(), dataFimProcesso.getTime()) - dataInicioProcesso.getTime()) / MS_PER_DAY) + 1;
const percentualDias = diasDecorridos / diasTotaisProcesso;
planejado = percentualDias * pesoTotalPlanejado;
} else if (data > dataFimProcesso) {
// Após o fim planejado, manter o peso total planejado
planejado = pesoTotalPlanejado;
}
// Antes do início planejado, o planejado fica 0
// Calcular progresso real até esta data - soma dos pesos apontados
const apontamentosAteData = apontamentosProcesso.filter(a => a.data_apontamento <= dataStr);
let realizado = 0;
apontamentosAteData.forEach(a => {
let pesoUnitario = 0;
if (a.tipo_apontamento === 'componente' && a.componente?.peso_unitario) {
pesoUnitario = Number(a.componente.peso_unitario);
} else if (a.tipo_apontamento === 'peca' && a.peca?.peso_unitario) {
pesoUnitario = Number(a.peca.peso_unitario);
}
realizado += pesoUnitario * Number(a.quantidade_produzida);
});
dadosGrafico.push({
data: dataStr,
planejado,
realizado
});
}
return {
id: processo.id,
nome: processo.nome,
pesoTotal: pesoTotalPlanejado, // SEMPRE o peso total da OF/ficha técnica
pesoFabricado: pesoFabricadoProcesso,
progressoReal,
progressoEsperado,
status,
dadosGrafico
};
}));
// Calcular progresso geral como média dos progressos reais, excluindo "Aceite/DB" e "Concluído"
const processosParaMedia = processos.filter(p =>
!p.nome.toLowerCase().includes('aceite') &&
!p.nome.toLowerCase().includes('db') &&
!p.nome.toLowerCase().includes('concluído') &&
!p.nome.toLowerCase().includes('concluido')
);
if (processosParaMedia.length > 0) {
const somaProgressos = processosParaMedia.reduce((soma, processo) => soma + processo.progressoReal, 0);
progressoGeral = somaProgressos / processosParaMedia.length;
} else {
progressoGeral = 0;
}
// Atualizar progresso real e esperado do processo "Concluído" com o progresso geral
processos.forEach(processo => {
if (processo.nome.toLowerCase().includes('concluído') || processo.nome.toLowerCase().includes('concluido')) {
processo.progressoReal = progressoGeral;
// Calcular progresso esperado do processo "Concluído" como média dos outros processos
// (exceto Aceite/DB e o próprio Concluído)
const processosParaMediaEsperada = processos.filter(p =>
!p.nome.toLowerCase().includes('aceite') &&
!p.nome.toLowerCase().includes('db') &&
!p.nome.toLowerCase().includes('concluído') &&
!p.nome.toLowerCase().includes('concluido')
);
if (processosParaMediaEsperada.length > 0) {
const somaProgressosEsperados = processosParaMediaEsperada.reduce((soma, proc) => soma + proc.progressoEsperado, 0);
processo.progressoEsperado = somaProgressosEsperados / processosParaMediaEsperada.length;
} else {
processo.progressoEsperado = 0;
}
// Recalcular status do processo "Concluído"
if (processo.progressoEsperado > 0) {
if (processo.progressoReal > processo.progressoEsperado) {
processo.status = 'azul'; // Adiantado
} else {
const percentualDoEsperado = (processo.progressoReal / processo.progressoEsperado) * 100;
if (percentualDoEsperado < 60) {
processo.status = 'vermelho'; // Atrasado
} else if (percentualDoEsperado < 80) {
processo.status = 'amarelo'; // Atenção
} else {
processo.status = 'verde'; // No prazo
}
}
}
console.log(`PROCESSO CONCLUÍDO atualizado: progresso real = ${progressoGeral.toFixed(2)}%, esperado = ${processo.progressoEsperado.toFixed(2)}%, status = ${processo.status}`);
}
});
console.log(`PROGRESSO GERAL CORRIGIDO: Média de ${processosParaMedia.length} processos (excluindo Aceite/DB e Concluído) = ${progressoGeral.toFixed(2)}%`);
console.log('Processos incluídos na média:', processosParaMedia.map(p => `${p.nome}: ${p.progressoReal.toFixed(1)}%`).join(', '));
const result = {
of: ofNumber,
progressoGeral,
pesoTotalFabricado,
tonelagem: pesoTotalPlanejado, // SEMPRE o peso total da OF/ficha técnica
processos,
dadosConsolidados: consolidatedData || [],
dateRange: dateRangeData?.[0] || { data_inicio_grafico: '', data_fim_grafico: '' }
};
console.log('=== RESULTADO FINAL DO DASHBOARD ===');
console.log('OF:', ofNumber);
console.log('Peso total planejado (base):', pesoTotalPlanejado, 'kg =', (pesoTotalPlanejado / 1000).toFixed(2), 't');
console.log('Peso total fabricado:', pesoTotalFabricado, 'kg =', (pesoTotalFabricado / 1000).toFixed(2), 't');
console.log('Progresso geral:', progressoGeral.toFixed(2) + '%');
console.log('=====================================');
setDashboardData(result);
} catch (error) {
console.error('Erro ao buscar dados do dashboard:', error);
toast.error('Erro ao carregar dados do dashboard');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchDashboardData();
}, [ofNumber]);
return {
dashboardData,
loading,
refetch: fetchDashboardData
};
};
+49
View File
@@ -0,0 +1,49 @@
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { logger } from '@/utils/logger';
interface DatabaseInfo {
database_size: string;
table_count: number;
}
export const useDatabaseUsage = () => {
const { data, isLoading, error } = useQuery({
queryKey: ['database-usage'],
queryFn: async () => {
try {
logger.info('Fetching database usage info');
const { data, error } = await supabase
.rpc('get_database_info');
if (error) {
logger.error('Database usage fetch error', error);
throw error;
}
if (!data || data.length === 0) {
logger.warn('No database info returned');
return null;
}
logger.success('Database usage fetched successfully');
return data[0] as DatabaseInfo;
} catch (err) {
logger.error('Database usage query failed', err);
throw err;
}
},
refetchInterval: 300000, // 5 minutos
staleTime: 240000, // 4 minutos
gcTime: 600000, // 10 minutos
retry: 2,
});
return {
databaseInfo: data,
loading: isLoading,
error
};
};
+18
View File
@@ -0,0 +1,18 @@
import { useState, useEffect } from 'react';
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
+238
View File
@@ -0,0 +1,238 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { useToast } from '@/hooks/use-toast';
export interface DiarioProducao {
id?: string;
data: string;
tecnico_responsavel: string;
turno: string;
observacoes_gerais?: string;
fotos_urls?: string[];
finalizado?: boolean;
created_at?: string;
updated_at?: string;
created_by?: string;
}
export interface RecursoProducao {
id: string;
nome: string;
tipo: 'maquina' | 'operario';
ativo?: boolean;
}
export interface ApontamentoDiarioRecurso {
id?: string;
diario_id: string;
of_number: string;
recurso_id: string;
qtd_inicio?: number;
qtd_meio?: number;
qtd_fim?: number;
qtd_segundo_turno?: number;
}
export interface LoteSoldaDiario {
id?: string;
diario_id: string;
of_number: string;
lote_solda?: string;
}
export interface OcorrenciaImprodutividade {
id: string;
descricao: string;
categoria?: string;
ativo?: boolean;
}
export const useDiarioProducao = () => {
const { toast } = useToast();
const queryClient = useQueryClient();
// Buscar todos os diários
const { data: diarios, isLoading: isLoadingDiarios } = useQuery({
queryKey: ['diarios-producao'],
queryFn: async () => {
const { data, error } = await supabase
.from('diarios_producao')
.select('*')
.order('data', { ascending: false });
if (error) throw error;
return data as DiarioProducao[];
}
});
// Buscar recursos
const { data: recursos, isLoading: isLoadingRecursos } = useQuery({
queryKey: ['recursos-producao'],
queryFn: async () => {
const { data, error } = await supabase
.from('recursos_producao')
.select('*')
.eq('ativo', true)
.order('tipo', { ascending: true })
.order('nome', { ascending: true });
if (error) throw error;
return data as RecursoProducao[];
}
});
// Buscar ocorrências
const { data: ocorrencias, isLoading: isLoadingOcorrencias } = useQuery({
queryKey: ['ocorrencias-improdutividade'],
queryFn: async () => {
const { data, error } = await supabase
.from('ocorrencias_improdutividade')
.select('*')
.eq('ativo', true)
.order('descricao', { ascending: true });
if (error) throw error;
return data as OcorrenciaImprodutividade[];
}
});
// Salvar diário
const salvarDiario = useMutation({
mutationFn: async (diario: DiarioProducao) => {
const { data, error } = await supabase
.from('diarios_producao')
.insert([{
data: diario.data,
tecnico_responsavel: diario.tecnico_responsavel,
turno: diario.turno,
observacoes_gerais: diario.observacoes_gerais,
fotos_urls: diario.fotos_urls,
finalizado: diario.finalizado || false
}])
.select()
.single();
if (error) throw error;
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['diarios-producao'] });
toast({
title: "Sucesso",
description: "Diário salvo com sucesso!"
});
},
onError: (error) => {
console.error('Erro ao salvar diário:', error);
toast({
title: "Erro",
description: "Erro ao salvar diário. Tente novamente.",
variant: "destructive"
});
}
});
// Salvar apontamentos de recursos
const salvarApontamentos = useMutation({
mutationFn: async (apontamentos: ApontamentoDiarioRecurso[]) => {
const { error } = await supabase
.from('apontamentos_diario_recursos')
.insert(apontamentos);
if (error) throw error;
},
onError: (error) => {
console.error('Erro ao salvar apontamentos:', error);
toast({
title: "Erro",
description: "Erro ao salvar apontamentos. Tente novamente.",
variant: "destructive"
});
}
});
// Salvar lotes de solda
const salvarLotesSolda = useMutation({
mutationFn: async (lotes: LoteSoldaDiario[]) => {
const { error } = await supabase
.from('lotes_solda_diario')
.insert(lotes);
if (error) throw error;
},
onError: (error) => {
console.error('Erro ao salvar lotes de solda:', error);
toast({
title: "Erro",
description: "Erro ao salvar lotes de solda. Tente novamente.",
variant: "destructive"
});
}
});
// Salvar ocorrências do diário
const salvarOcorrenciasDiario = useMutation({
mutationFn: async (dados: { diario_id: string; ocorrencia_ids: string[] }) => {
const ocorrenciasParaInserir = dados.ocorrencia_ids.map(ocorrencia_id => ({
diario_id: dados.diario_id,
ocorrencia_id
}));
const { error } = await supabase
.from('diario_ocorrencias')
.insert(ocorrenciasParaInserir);
if (error) throw error;
},
onError: (error) => {
console.error('Erro ao salvar ocorrências do diário:', error);
toast({
title: "Erro",
description: "Erro ao salvar ocorrências. Tente novamente.",
variant: "destructive"
});
}
});
// Deletar diário
const deletarDiario = useMutation({
mutationFn: async (id: string) => {
const { error } = await supabase
.from('diarios_producao')
.delete()
.eq('id', id);
if (error) throw error;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['diarios-producao'] });
toast({
title: "Sucesso",
description: "Diário deletado com sucesso!"
});
},
onError: (error) => {
console.error('Erro ao deletar diário:', error);
toast({
title: "Erro",
description: "Erro ao deletar diário. Tente novamente.",
variant: "destructive"
});
}
});
return {
diarios,
recursos,
ocorrencias,
isLoadingDiarios,
isLoadingRecursos,
isLoadingOcorrencias,
salvarDiario,
salvarApontamentos,
salvarLotesSolda,
salvarOcorrenciasDiario,
deletarDiario
};
};
+55
View File
@@ -0,0 +1,55 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
export const useEditarItemInsumo = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
itemId,
novaQuantidade,
pesoUnitario
}: {
itemId: string;
novaQuantidade: number;
pesoUnitario?: number;
}) => {
if (!itemId || typeof novaQuantidade !== 'number') {
throw new Error('Parâmetros inválidos para edição do item');
}
if (novaQuantidade < 0) {
throw new Error('Quantidade deve ser um valor positivo');
}
const updateData: any = {
quantidade_expedida: novaQuantidade
};
// Se tiver peso unitário, validar e calcular o peso total
if (pesoUnitario !== undefined) {
if (typeof pesoUnitario !== 'number' || pesoUnitario < 0) {
throw new Error('Peso unitário deve ser um valor positivo');
}
updateData.peso_total = novaQuantidade * pesoUnitario;
}
const { error } = await supabase
.from('itens_romaneio_insumos')
.update(updateData)
.eq('id', itemId);
if (error) throw error;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['romaneios'] });
toast.success('Quantidade de insumo atualizada com sucesso!');
},
onError: (error) => {
console.error('Erro ao editar quantidade de insumo:', error);
toast.error('Erro ao editar quantidade de insumo: ' + (error instanceof Error ? error.message : 'Erro desconhecido'));
},
});
};
+43
View File
@@ -0,0 +1,43 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
export const useEditarItemPeca = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ itemId, novaQuantidade, pesoUnitario }: {
itemId: string;
novaQuantidade: number;
pesoUnitario: number;
}) => {
if (!itemId || typeof novaQuantidade !== 'number' || typeof pesoUnitario !== 'number') {
throw new Error('Parâmetros inválidos para edição do item');
}
if (novaQuantidade < 0 || pesoUnitario < 0) {
throw new Error('Quantidade e peso devem ser valores positivos');
}
const { error } = await supabase
.from('itens_romaneio_pecas')
.update({
quantidade_expedida: novaQuantidade,
peso_total: novaQuantidade * pesoUnitario
})
.eq('id', itemId);
if (error) throw error;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['romaneios'] });
queryClient.invalidateQueries({ queryKey: ['pecas-pintura'] });
toast.success('Quantidade atualizada com sucesso!');
},
onError: (error) => {
console.error('Erro ao editar quantidade:', error);
toast.error('Erro ao editar quantidade: ' + (error instanceof Error ? error.message : 'Erro desconhecido'));
},
});
};
+200
View File
@@ -0,0 +1,200 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
export interface EmpenhoMaterial {
id: string;
material_id: string;
of_number: string;
quantidade_empenhada: number;
quantidade_utilizada: number;
lote?: string;
observacoes?: string;
status: 'Empenhado' | 'Finalizado' | 'Cancelado';
data_empenho: string;
created_by?: string;
created_at: string;
movimentacao_empenho_id?: string;
estoque_materiais?: {
codigo: string;
descricao: string;
unidade: string;
valor_unitario: number;
};
}
// Hook para buscar todos os empenhos
export const useEmpenhosMaterial = (ofNumber?: string) => {
return useQuery({
queryKey: ['empenhos-material', ofNumber],
queryFn: async () => {
let query = supabase
.from('empenhos_material')
.select(`
*,
estoque_materiais!inner(
codigo,
descricao,
unidade,
valor_unitario
)
`)
.order('created_at', { ascending: false });
if (ofNumber) {
query = query.eq('of_number', ofNumber);
}
const { data, error } = await query;
if (error) throw error;
return data as EmpenhoMaterial[];
}
});
};
// Hook para buscar empenhos ativos por material
export const useEmpenhosAtivosPorMaterial = (materialId: string) => {
return useQuery({
queryKey: ['empenhos-ativos-material', materialId],
queryFn: async () => {
const { data, error } = await supabase
.from('empenhos_material')
.select('*')
.eq('material_id', materialId)
.eq('status', 'Empenhado');
if (error) throw error;
return data as EmpenhoMaterial[];
}
});
};
// Hook para buscar OFs com empenhos
export const useOFsComEmpenhos = () => {
return useQuery({
queryKey: ['ofs-com-empenhos'],
queryFn: async () => {
const { data, error } = await supabase
.from('empenhos_material')
.select('of_number')
.not('of_number', 'is', null)
.neq('status', 'Cancelado');
if (error) throw error;
const uniqueOFs = Array.from(new Set(data.map(item => item.of_number)))
.filter(of => of && of.trim() !== '')
.sort();
return uniqueOFs;
}
});
};
export const useCancelarEmpenho = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (empenhoId: string) => {
console.log('🗑️ Iniciando cancelamento do empenho:', empenhoId);
// Buscar dados do empenho
const { data: empenho, error: fetchError } = await supabase
.from('empenhos_material')
.select('*, movimentacao_empenho_id, estoque_materiais(codigo, descricao)')
.eq('id', empenhoId)
.single();
if (fetchError) {
console.error('Erro ao buscar empenho:', fetchError);
throw fetchError;
}
if (!empenho) throw new Error('Empenho não encontrado');
if (empenho.quantidade_utilizada > 0) {
throw new Error('Não é possível cancelar empenho que já foi parcialmente utilizado');
}
console.log('📋 Dados do empenho encontrado:', empenho);
// Cancelar o empenho primeiro
const { error: updateEmpenhoError } = await supabase
.from('empenhos_material')
.update({
status: 'Cancelado',
movimentacao_empenho_id: null
})
.eq('id', empenhoId);
if (updateEmpenhoError) {
console.error('Erro ao cancelar empenho:', updateEmpenhoError);
throw new Error('Erro ao cancelar empenho');
}
// Se há movimentação vinculada, tentar excluí-la também
if (empenho.movimentacao_empenho_id) {
console.log('📦 Excluindo movimentação vinculada:', empenho.movimentacao_empenho_id);
try {
const { error: deleteMovError } = await supabase
.from('movimentacoes_estoque')
.delete()
.eq('id', empenho.movimentacao_empenho_id);
if (deleteMovError) {
console.error('Erro ao excluir movimentação:', deleteMovError);
// Não falha aqui, apenas loga o erro
console.log('⚠️ Movimentação não pôde ser excluída, mas empenho foi cancelado');
} else {
console.log('✅ Movimentação excluída com sucesso');
}
} catch (movError) {
console.error('Erro ao tentar excluir movimentação:', movError);
// Continua mesmo se não conseguir excluir a movimentação
}
}
console.log('✅ Empenho cancelado com sucesso');
return { success: true, empenho };
},
onSuccess: (result) => {
toast.success('Empenho cancelado com sucesso!');
queryClient.invalidateQueries({ queryKey: ['empenhos-material'] });
queryClient.invalidateQueries({ queryKey: ['estoque-materiais'] });
queryClient.invalidateQueries({ queryKey: ['movimentacoes-estoque'] });
},
onError: (error: Error) => {
console.error('Erro ao cancelar empenho:', error);
toast.error(error.message || 'Erro ao cancelar empenho');
}
});
};
export const useRelatorioEmpenhosPorOF = (ofNumber?: string) => {
return useQuery({
queryKey: ['relatorio-empenhos-of', ofNumber],
queryFn: async () => {
if (!ofNumber) return [];
const { data, error } = await supabase
.from('empenhos_material')
.select(`
*,
estoque_materiais!inner(
codigo,
descricao,
unidade,
valor_unitario
)
`)
.eq('of_number', ofNumber)
.neq('status', 'Cancelado')
.order('created_at', { ascending: false });
if (error) throw error;
return data as EmpenhoMaterial[];
},
enabled: !!ofNumber
});
};
@@ -0,0 +1,104 @@
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
export interface EmpenhoMaterial {
id: string;
material_id: string;
of_number: string;
quantidade_empenhada: number;
quantidade_utilizada: number;
lote?: string;
observacoes?: string;
status: 'Empenhado' | 'Finalizado' | 'Cancelado';
data_empenho: string;
created_by?: string;
created_at: string;
movimentacao_empenho_id?: string;
estoque_materiais?: {
codigo: string;
descricao: string;
unidade: string;
valor_unitario: number;
};
}
// Hook para buscar todos os empenhos (apenas visualização)
export const useEmpenhosMaterial = (ofNumber?: string) => {
return useQuery({
queryKey: ['empenhos-material', ofNumber],
queryFn: async () => {
let query = supabase
.from('empenhos_material')
.select(`
*,
estoque_materiais!inner(
codigo,
descricao,
unidade,
valor_unitario
)
`)
.order('created_at', { ascending: false });
if (ofNumber) {
query = query.eq('of_number', ofNumber);
}
const { data, error } = await query;
if (error) throw error;
return data as EmpenhoMaterial[];
}
});
};
// Hook para buscar OFs com empenhos
export const useOFsComEmpenhos = () => {
return useQuery({
queryKey: ['ofs-com-empenhos'],
queryFn: async () => {
const { data, error } = await supabase
.from('empenhos_material')
.select('of_number')
.not('of_number', 'is', null)
.neq('status', 'Cancelado');
if (error) throw error;
const uniqueOFs = Array.from(new Set(data.map(item => item.of_number)))
.filter(of => of && of.trim() !== '')
.sort();
return uniqueOFs;
}
});
};
// Hook para relatório de empenhos por OF
export const useRelatorioEmpenhosPorOF = (ofNumber?: string) => {
return useQuery({
queryKey: ['relatorio-empenhos-of', ofNumber],
queryFn: async () => {
if (!ofNumber) return [];
const { data, error } = await supabase
.from('empenhos_material')
.select(`
*,
estoque_materiais!inner(
codigo,
descricao,
unidade,
valor_unitario
)
`)
.eq('of_number', ofNumber)
.neq('status', 'Cancelado')
.order('created_at', { ascending: false });
if (error) throw error;
return data as EmpenhoMaterial[];
},
enabled: !!ofNumber
});
};
+201
View File
@@ -0,0 +1,201 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
import { useState } from 'react';
export interface Equipamento {
id: string;
codigo: string;
descricao: string;
capacidade?: string;
quantidade: number;
local_estoque: string;
propriedade: 'proprio' | 'terceiros' | 'alugado';
validade_calibracao?: string;
certificado_calibracao?: string;
periodicidade_calibracao?: number;
of_number?: string;
destino_outro?: string;
data_saida?: string;
retirado_por?: string;
data_retorno?: string;
devolvido_por?: string;
observacoes?: string;
created_by?: string;
created_at: string;
updated_at: string;
}
export interface EquipamentoFormData {
codigo: string;
descricao: string;
capacidade?: string;
quantidade: number;
local_estoque: string;
propriedade: 'proprio' | 'terceiros' | 'alugado';
validade_calibracao?: string;
certificado_calibracao?: string;
periodicidade_calibracao?: number;
of_number?: string;
destino_outro?: string;
data_saida?: string;
retirado_por?: string;
data_retorno?: string;
devolvido_por?: string;
observacoes?: string;
}
interface EquipamentoFilters {
status: string;
propriedade: string;
search: string;
}
export const useEquipamentos = () => {
const queryClient = useQueryClient();
const [filters, setFilters] = useState<EquipamentoFilters>({
status: 'all',
propriedade: 'all',
search: ''
});
const {
data: equipamentos = [],
isLoading,
error,
} = useQuery({
queryKey: ['equipamentos'],
queryFn: async () => {
const { data, error } = await supabase
.from('equipamentos')
.select('*')
.order('codigo', { ascending: true });
if (error) throw error;
return data as Equipamento[];
},
});
const createEquipamento = useMutation({
mutationFn: async (data: EquipamentoFormData) => {
const { data: result, error } = await supabase
.from('equipamentos')
.insert(data)
.select()
.single();
if (error) throw error;
return result;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['equipamentos'] });
toast.success('Equipamento criado com sucesso!');
},
onError: (error) => {
console.error('Erro ao criar equipamento:', error);
toast.error('Erro ao criar equipamento');
},
});
const updateEquipamento = useMutation({
mutationFn: async ({ id, data }: { id: string; data: Partial<EquipamentoFormData> }) => {
const { data: result, error } = await supabase
.from('equipamentos')
.update(data)
.eq('id', id)
.select()
.single();
if (error) throw error;
return result;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['equipamentos'] });
toast.success('Equipamento atualizado com sucesso!');
},
onError: (error) => {
console.error('Erro ao atualizar equipamento:', error);
toast.error('Erro ao atualizar equipamento');
},
});
const deleteEquipamento = useMutation({
mutationFn: async (id: string) => {
const { error } = await supabase
.from('equipamentos')
.delete()
.eq('id', id);
if (error) throw error;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['equipamentos'] });
toast.success('Equipamento excluído com sucesso!');
},
onError: (error) => {
console.error('Erro ao excluir equipamento:', error);
toast.error('Erro ao excluir equipamento');
},
});
// Função para calcular o status do equipamento
const getEquipamentoStatus = (equipamento: Equipamento) => {
const today = new Date();
today.setHours(0, 0, 0, 0);
const calibrationDate = equipamento.validade_calibracao
? new Date(equipamento.validade_calibracao + 'T00:00:00')
: null;
if (calibrationDate && calibrationDate < today) {
return { text: 'Calibração Vencida', class: 'bg-red-500/20 text-red-400 border-red-500/30', key: 'calibracao_vencida' };
}
if (equipamento.data_saida && !equipamento.data_retorno) {
return { text: 'Em Uso', class: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30', key: 'em_uso' };
}
return { text: 'Disponível', class: 'bg-green-500/20 text-green-400 border-green-500/30', key: 'disponivel' };
};
// Estatísticas dos equipamentos
const stats = {
total: equipamentos.length,
disponiveis: equipamentos.filter(eq => getEquipamentoStatus(eq).key === 'disponivel').length,
emUso: equipamentos.filter(eq => getEquipamentoStatus(eq).key === 'em_uso').length,
calibracaoVencida: equipamentos.filter(eq => {
if (!eq.validade_calibracao) return false;
const today = new Date();
const next30Days = new Date();
next30Days.setDate(today.getDate() + 30);
const calDate = new Date(eq.validade_calibracao + 'T00:00:00');
return calDate < next30Days;
}).length
};
const updateFilters = (newFilters: Partial<EquipamentoFilters>) => {
setFilters(prev => ({ ...prev, ...newFilters }));
};
const clearFilters = () => {
setFilters({
status: 'all',
propriedade: 'all',
search: ''
});
};
return {
equipamentos,
isLoading,
loading: isLoading, // Alias para compatibilidade
error,
filters,
updateFilters,
clearFilters,
createEquipamento,
updateEquipamento,
deleteEquipamento,
getEquipamentoStatus,
stats,
};
};
+420
View File
@@ -0,0 +1,420 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { useState, useMemo } from 'react';
import { toast } from 'sonner';
export interface EstoqueMaterial {
id: string;
codigo: string;
descricao: string;
tipo_material_id?: string;
unidade: string;
quantidade_total: number;
quantidade_disponivel: number;
quantidade_empenhada: number;
quantidade_minima: number;
quantidade_maxima?: number;
peso_unitario: number;
valor_unitario?: number;
lote_atual?: string;
fornecedor?: string;
localizacao?: string;
status: 'Normal' | 'Crítico' | 'Excesso';
certificado?: string;
observacoes?: string;
comprimento?: number;
largura?: number;
espessura?: number;
qualidade_aco?: string;
kg_por_metro?: number;
created_at: string;
updated_at: string;
created_by?: string;
tipos_materia_prima?: {
nome: string;
descricao?: string;
categoria?: string;
gestao_estoque_critico?: boolean;
} | null;
}
export const useEstoqueMateriais = () => {
return useQuery({
queryKey: ['estoque-materiais'],
queryFn: async () => {
const { data, error } = await supabase
.from('estoque_materiais')
.select(`
*,
tipos_materia_prima (
nome,
descricao,
categoria,
gestao_estoque_critico
)
`)
.order('codigo');
if (error) throw error;
// Transform the raw data to match our EstoqueMaterial interface
const transformedData = (data || []).map(item => {
// Ensure tipos_materia_prima is properly typed
const tiposMaterial = item.tipos_materia_prima
? {
nome: item.tipos_materia_prima.nome || '',
descricao: item.tipos_materia_prima.descricao || undefined,
categoria: item.tipos_materia_prima.categoria || undefined,
gestao_estoque_critico: item.tipos_materia_prima.gestao_estoque_critico ?? true
}
: null;
return {
...item,
tipos_materia_prima: tiposMaterial
};
}) as EstoqueMaterial[];
return transformedData;
},
});
};
export const useTiposMateriaPrima = () => {
return useQuery({
queryKey: ['tipos-materia-prima'],
queryFn: async () => {
const { data, error } = await supabase
.from('tipos_materia_prima')
.select('*')
.eq('ativo', true)
.order('nome');
if (error) throw error;
return data;
},
});
};
export const useCriarMaterial = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (material: {
codigo: string;
descricao: string;
tipo_material_id?: string;
unidade: string;
quantidade_total: number;
quantidade_disponivel: number;
quantidade_empenhada: number;
quantidade_minima: number;
quantidade_maxima?: number;
peso_unitario: number;
valor_unitario?: number;
lote_atual?: string;
fornecedor?: string;
localizacao?: string;
status: 'Normal' | 'Crítico' | 'Excesso';
certificado?: string;
observacoes?: string;
comprimento?: number;
largura?: number;
espessura?: number;
qualidade_aco?: string;
kg_por_metro?: number;
created_by?: string;
}) => {
const { data, error } = await supabase
.from('estoque_materiais')
.insert(material)
.select();
if (error) throw error;
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['estoque-materiais'] });
toast.success('Material criado com sucesso');
},
onError: (error) => {
console.error('Erro ao criar material:', error);
toast.error('Erro ao criar material');
}
});
};
export const useAtualizarMaterial = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (material: Partial<EstoqueMaterial> & { id: string }) => {
const { id, tipos_materia_prima, created_at, updated_at, ...updateData } = material;
const { error } = await supabase
.from('estoque_materiais')
.update(updateData)
.eq('id', id);
if (error) throw error;
return material;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['estoque-materiais'] });
toast.success('Material atualizado com sucesso');
},
onError: (error) => {
console.error('Erro ao atualizar material:', error);
toast.error('Erro ao atualizar material');
}
});
};
export const useExcluirMateriais = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (materialIds: string[]) => {
const { error } = await supabase
.from('estoque_materiais')
.delete()
.in('id', materialIds);
if (error) throw error;
return materialIds;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['estoque-materiais'] });
toast.success('Materiais excluídos com sucesso');
},
onError: (error) => {
console.error('Erro ao excluir materiais:', error);
toast.error('Erro ao excluir materiais');
}
});
};
export const useDuplicarMaterial = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (material: EstoqueMaterial) => {
const { id, created_at, updated_at, tipos_materia_prima, ...materialData } = material;
const duplicatedMaterial = {
...materialData,
codigo: `${material.codigo}_COPY`,
descricao: `${material.descricao} (Cópia)`
};
const { data, error } = await supabase
.from('estoque_materiais')
.insert(duplicatedMaterial)
.select();
if (error) throw error;
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['estoque-materiais'] });
toast.success('Material duplicado com sucesso');
},
onError: (error) => {
console.error('Erro ao duplicar material:', error);
toast.error('Erro ao duplicar material');
}
});
};
export const useMovimentacoesEstoque = () => {
return useQuery({
queryKey: ['movimentacoes-estoque'],
queryFn: async () => {
const { data, error } = await supabase
.from('movimentacoes_estoque')
.select(`
*,
estoque_materiais (
codigo,
descricao
)
`)
.order('created_at', { ascending: false });
if (error) throw error;
return data;
},
});
};
export const useCriarTipoMaterial = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (tipo: {
nome: string;
descricao?: string;
categoria: string;
gestao_estoque_critico: boolean;
caracteristicas: any;
controles: any;
ativo: boolean;
}) => {
const { data, error } = await supabase
.from('tipos_materia_prima')
.insert([tipo]);
if (error) throw error;
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tipos-materia-prima'] });
toast.success('Tipo de material criado com sucesso');
},
onError: (error) => {
console.error('Erro ao criar tipo de material:', error);
toast.error('Erro ao criar tipo de material');
}
});
};
export const useAtualizarTipoMaterial = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (tipo: {
id: string;
nome: string;
descricao?: string;
categoria?: string;
caracteristicas?: any;
controles?: any;
ativo?: boolean;
gestao_estoque_critico?: boolean;
}) => {
const { error } = await supabase
.from('tipos_materia_prima')
.update({
nome: tipo.nome,
descricao: tipo.descricao,
categoria: tipo.categoria,
caracteristicas: tipo.caracteristicas || {},
controles: tipo.controles || {},
ativo: tipo.ativo ?? true,
gestao_estoque_critico: tipo.gestao_estoque_critico ?? true
})
.eq('id', tipo.id);
if (error) throw error;
return tipo;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tipos-materia-prima'] });
toast.success('Tipo de material atualizado com sucesso');
},
onError: (error) => {
console.error('Erro ao atualizar tipo de material:', error);
toast.error('Erro ao atualizar tipo de material');
}
});
};
export const useExcluirTipoMaterial = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (id: string) => {
const { error } = await supabase
.from('tipos_materia_prima')
.delete()
.eq('id', id);
if (error) throw error;
return id;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tipos-materia-prima'] });
toast.success('Tipo de material excluído com sucesso');
},
onError: (error) => {
console.error('Erro ao excluir tipo de material:', error);
toast.error('Erro ao excluir tipo de material');
}
});
};
export const useEstoque = () => {
const [tiposSelecionados, setTiposSelecionados] = useState<string[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const { data: materiais = [], isLoading: loading, refetch } = useEstoqueMateriais();
// Recalcular status baseado na gestão de estoque crítico
const materiaisComStatusAtualizado = useMemo(() => {
return materiais.map(material => {
const temGestaoAtivada = material.tipos_materia_prima?.gestao_estoque_critico !== false;
let status = material.status;
// Se não tem gestão ativada, nunca pode ser crítico
if (!temGestaoAtivada && status === 'Crítico') {
if (material.quantidade_maxima && material.quantidade_disponivel >= material.quantidade_maxima) {
status = 'Excesso';
} else {
status = 'Normal';
}
}
// Se tem gestão ativada, recalcular normalmente
else if (temGestaoAtivada) {
if (material.quantidade_disponivel <= material.quantidade_minima && material.quantidade_minima > 0) {
status = 'Crítico';
} else if (material.quantidade_maxima && material.quantidade_disponivel >= material.quantidade_maxima) {
status = 'Excesso';
} else {
status = 'Normal';
}
}
return {
...material,
status
};
});
}, [materiais]);
// Calculate stats
const stats = useMemo(() => {
const totalKg = materiaisComStatusAtualizado.reduce((acc, material) => {
const kgPorMetro = material.kg_por_metro || 0;
const comprimento = material.comprimento || 0;
const quantidadeTotal = material.quantidade_total || 0;
const kgItem = (kgPorMetro * comprimento / 1000) * quantidadeTotal;
return acc + kgItem;
}, 0);
const totalUnidades = materiaisComStatusAtualizado.reduce((acc, material) => acc + (material.quantidade_total || 0), 0);
const totalDisponiveis = materiaisComStatusAtualizado.reduce((acc, material) => acc + (material.quantidade_disponivel || 0), 0);
const totalEmpenhadas = materiaisComStatusAtualizado.reduce((acc, material) => acc + (material.quantidade_empenhada || 0), 0);
return {
totalKg,
totalUnidades,
totalDisponiveis,
totalEmpenhadas
};
}, [materiaisComStatusAtualizado]);
const refreshData = () => {
refetch();
};
return {
materiais: materiaisComStatusAtualizado,
loading,
tiposSelecionados,
searchTerm,
setSearchTerm,
setTiposSelecionados,
stats,
refreshData
};
};
+293
View File
@@ -0,0 +1,293 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
// Interfaces
export interface UnidadeMedida {
id: string;
nome: string;
abreviacao: string;
descricao?: string;
ativo: boolean;
}
export interface LocalizacaoEstoque {
id: string;
nome: string;
codigo?: string;
descricao?: string;
ativo: boolean;
}
export interface QualidadeAco {
id: string;
nome: string;
norma?: string;
descricao?: string;
propriedades?: any;
ativo: boolean;
}
// Hooks para Unidades de Medida
export const useUnidadesMedida = () => {
return useQuery({
queryKey: ['unidades-medida'],
queryFn: async () => {
const { data, error } = await supabase
.from('unidades_medida')
.select('*')
.eq('ativo', true)
.order('nome');
if (error) throw error;
return data as UnidadeMedida[];
},
});
};
export const useCriarUnidadeMedida = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data: Omit<UnidadeMedida, 'id'>) => {
const { data: result, error } = await supabase
.from('unidades_medida')
.insert(data)
.select()
.single();
if (error) throw error;
return result;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['unidades-medida'] });
toast.success('Unidade de medida criada com sucesso!');
},
onError: (error: any) => {
toast.error('Erro ao criar unidade de medida: ' + error.message);
},
});
};
export const useAtualizarUnidadeMedida = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, ...data }: UnidadeMedida) => {
const { data: result, error } = await supabase
.from('unidades_medida')
.update(data)
.eq('id', id)
.select()
.single();
if (error) throw error;
return result;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['unidades-medida'] });
toast.success('Unidade de medida atualizada com sucesso!');
},
onError: (error: any) => {
toast.error('Erro ao atualizar unidade de medida: ' + error.message);
},
});
};
export const useExcluirUnidadeMedida = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (id: string) => {
const { error } = await supabase
.from('unidades_medida')
.update({ ativo: false })
.eq('id', id);
if (error) throw error;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['unidades-medida'] });
toast.success('Unidade de medida desativada com sucesso!');
},
onError: (error: any) => {
toast.error('Erro ao desativar unidade de medida: ' + error.message);
},
});
};
// Hooks para Localizações
export const useLocalizacoesEstoque = () => {
return useQuery({
queryKey: ['localizacoes-estoque'],
queryFn: async () => {
const { data, error } = await supabase
.from('localizacoes_estoque')
.select('*')
.eq('ativo', true)
.order('nome');
if (error) throw error;
return data as LocalizacaoEstoque[];
},
});
};
export const useCriarLocalizacao = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data: Omit<LocalizacaoEstoque, 'id'>) => {
const { data: result, error } = await supabase
.from('localizacoes_estoque')
.insert(data)
.select()
.single();
if (error) throw error;
return result;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['localizacoes-estoque'] });
toast.success('Localização criada com sucesso!');
},
onError: (error: any) => {
toast.error('Erro ao criar localização: ' + error.message);
},
});
};
export const useAtualizarLocalizacao = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, ...data }: LocalizacaoEstoque) => {
const { data: result, error } = await supabase
.from('localizacoes_estoque')
.update(data)
.eq('id', id)
.select()
.single();
if (error) throw error;
return result;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['localizacoes-estoque'] });
toast.success('Localização atualizada com sucesso!');
},
onError: (error: any) => {
toast.error('Erro ao atualizar localização: ' + error.message);
},
});
};
export const useExcluirLocalizacao = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (id: string) => {
const { error } = await supabase
.from('localizacoes_estoque')
.update({ ativo: false })
.eq('id', id);
if (error) throw error;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['localizacoes-estoque'] });
toast.success('Localização desativada com sucesso!');
},
onError: (error: any) => {
toast.error('Erro ao desativar localização: ' + error.message);
},
});
};
// Hooks para Qualidade do Aço
export const useQualidadesAco = () => {
return useQuery({
queryKey: ['qualidades-aco'],
queryFn: async () => {
const { data, error } = await supabase
.from('qualidades_aco')
.select('*')
.eq('ativo', true)
.order('nome');
if (error) throw error;
return data as QualidadeAco[];
},
});
};
export const useCriarQualidadeAco = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data: Omit<QualidadeAco, 'id'>) => {
const { data: result, error } = await supabase
.from('qualidades_aco')
.insert(data)
.select()
.single();
if (error) throw error;
return result;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['qualidades-aco'] });
toast.success('Qualidade de aço criada com sucesso!');
},
onError: (error: any) => {
toast.error('Erro ao criar qualidade de aço: ' + error.message);
},
});
};
export const useAtualizarQualidadeAco = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, ...data }: QualidadeAco) => {
const { data: result, error } = await supabase
.from('qualidades_aco')
.update(data)
.eq('id', id)
.select()
.single();
if (error) throw error;
return result;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['qualidades-aco'] });
toast.success('Qualidade de aço atualizada com sucesso!');
},
onError: (error: any) => {
toast.error('Erro ao atualizar qualidade de aço: ' + error.message);
},
});
};
export const useExcluirQualidadeAco = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (id: string) => {
const { error } = await supabase
.from('qualidades_aco')
.update({ ativo: false })
.eq('id', id);
if (error) throw error;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['qualidades-aco'] });
toast.success('Qualidade de aço desativada com sucesso!');
},
onError: (error: any) => {
toast.error('Erro ao desativar qualidade de aço: ' + error.message);
},
});
};
+197
View File
@@ -0,0 +1,197 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
export interface MovimentacaoEstoque {
id: string;
material_id: string;
tipo_movimentacao: 'entrada' | 'saida' | 'transferencia' | 'ajuste' | 'empenho' | 'desempenho';
quantidade: number;
valor_unitario?: number;
valor_total?: number;
lote?: string;
fornecedor?: string;
nota_fiscal?: string;
of_vinculada?: string;
observacoes?: string;
data_movimentacao: string;
created_at: string;
created_by?: string;
user_name?: string;
estoque_materiais?: {
codigo: string;
descricao: string;
};
}
export const useMovimentacoesEstoque = () => {
return useQuery({
queryKey: ['movimentacoes-estoque'],
queryFn: async () => {
const { data, error } = await supabase
.from('movimentacoes_estoque')
.select(`
*,
estoque_materiais (
codigo,
descricao
)
`)
.order('created_at', { ascending: false });
if (error) throw error;
return data as MovimentacaoEstoque[];
},
});
};
export const useCriarMovimentacao = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (movimentacao: {
material_id: string;
tipo_movimentacao: 'entrada' | 'saida' | 'transferencia' | 'ajuste' | 'empenho' | 'desempenho';
quantidade: number;
lote?: string;
fornecedor?: string;
nota_fiscal?: string;
of_vinculada?: string;
observacoes?: string;
data_movimentacao?: string;
valor_unitario?: number;
user_name?: string;
}) => {
// Validações básicas
if (!movimentacao.material_id) {
throw new Error('Material é obrigatório');
}
if (movimentacao.quantidade <= 0) {
throw new Error('Quantidade deve ser maior que zero');
}
// Validações específicas para empenho e desempenho
if (movimentacao.tipo_movimentacao === 'empenho' && !movimentacao.of_vinculada) {
throw new Error('OF vinculada é obrigatória para movimentações de empenho');
}
if (movimentacao.tipo_movimentacao === 'desempenho' && !movimentacao.of_vinculada) {
throw new Error('OF vinculada é obrigatória para movimentações de desempenho');
}
// Para empenhos e saídas, verificar disponibilidade
if (movimentacao.tipo_movimentacao === 'empenho' || movimentacao.tipo_movimentacao === 'saida') {
const { data: materialAtual, error: materialError } = await supabase
.from('estoque_materiais')
.select('quantidade_disponivel, descricao, codigo')
.eq('id', movimentacao.material_id)
.single();
if (materialError) {
throw new Error('Material não encontrado');
}
if (!materialAtual) {
throw new Error('Material não encontrado');
}
if (materialAtual.quantidade_disponivel < movimentacao.quantidade) {
const errorMsg = `Quantidade insuficiente para ${materialAtual.codigo} - ${materialAtual.descricao}. Disponível: ${materialAtual.quantidade_disponivel}, Solicitado: ${movimentacao.quantidade}`;
throw new Error(errorMsg);
}
}
// Preparar dados para inserção
const { data: currentUser } = await supabase.auth.getUser();
const dadosInsercao = {
material_id: movimentacao.material_id,
tipo_movimentacao: movimentacao.tipo_movimentacao,
quantidade: movimentacao.quantidade,
lote: movimentacao.lote || null,
fornecedor: movimentacao.fornecedor || null,
nota_fiscal: movimentacao.nota_fiscal || null,
of_vinculada: movimentacao.of_vinculada || null,
observacoes: movimentacao.observacoes || null,
data_movimentacao: movimentacao.data_movimentacao || new Date().toISOString().split('T')[0],
valor_unitario: movimentacao.valor_unitario || null,
created_by: currentUser.user?.id
};
// Criar a movimentação - o trigger do banco cuidará dos empenhos automaticamente
const { data: movimentacaoData, error: movError } = await supabase
.from('movimentacoes_estoque')
.insert([dadosInsercao])
.select()
.single();
if (movError) {
console.error('Erro detalhado:', movError);
throw new Error(movError.message || 'Erro ao criar movimentação');
}
if (!movimentacaoData) {
throw new Error('Nenhum dado foi retornado após a criação');
}
return movimentacaoData;
},
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['estoque-materiais'] });
queryClient.invalidateQueries({ queryKey: ['movimentacoes-estoque'] });
queryClient.invalidateQueries({ queryKey: ['empenhos-material'] });
toast.success('Movimentação criada com sucesso!');
},
onError: (error: Error) => {
console.error('Erro capturado na mutação:', error);
toast.error(error.message || 'Erro ao criar movimentação');
}
});
};
export const useExcluirMovimentacao = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (movimentacaoId: string) => {
// Buscar detalhes da movimentação
const { data: movimentacao, error: fetchError } = await supabase
.from('movimentacoes_estoque')
.select('*, estoque_materiais(codigo, descricao)')
.eq('id', movimentacaoId)
.single();
if (fetchError) {
throw new Error('Movimentação não encontrada');
}
// Excluir a movimentação - o trigger cuidará da reversão
const { error: deleteError } = await supabase
.from('movimentacoes_estoque')
.delete()
.eq('id', movimentacaoId);
if (deleteError) {
throw deleteError;
}
return movimentacao;
},
onSuccess: (movimentacao) => {
queryClient.invalidateQueries({ queryKey: ['movimentacoes-estoque'] });
queryClient.invalidateQueries({ queryKey: ['estoque-materiais'] });
queryClient.invalidateQueries({ queryKey: ['empenhos-material'] });
const materialInfo = movimentacao.estoque_materiais;
const materialDesc = materialInfo ? `${materialInfo.codigo} - ${materialInfo.descricao}` : 'Material';
toast.success(`Movimentação de ${movimentacao.tipo_movimentacao} para ${materialDesc} excluída com sucesso!`);
},
onError: (error: any) => {
console.error('Erro ao excluir movimentação:', error);
const errorMessage = error?.message || 'Erro ao excluir movimentação';
toast.error(errorMessage);
}
});
};
+61
View File
@@ -0,0 +1,61 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
import { perfilsData } from '@/utils/perfilsData';
export const usePopulatePerfisMateriais = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => {
const materiaisData = perfilsData.map(perfil => ({
codigo: perfil.descricao,
descricao: perfil.descricao,
tipo_material_id: null,
unidade: 'KG',
peso_unitario: perfil.peso,
quantidade_total: 10,
quantidade_disponivel: 10,
quantidade_empenhada: 0,
quantidade_minima: 1,
quantidade_maxima: null,
lote_atual: null,
fornecedor: null,
localizacao: null,
status: 'Normal',
certificado: null,
observacoes: 'Perfil W/HP - Cadastrado automaticamente'
}));
// Verificar se já existem dados para evitar duplicação
const { data: existingMaterials } = await supabase
.from('estoque_materiais')
.select('codigo')
.in('codigo', materiaisData.map(m => m.codigo));
const existingCodes = existingMaterials?.map(m => m.codigo) || [];
const newMaterials = materiaisData.filter(m => !existingCodes.includes(m.codigo));
if (newMaterials.length === 0) {
throw new Error('Todos os perfis já foram cadastrados anteriormente');
}
const { data, error } = await supabase
.from('estoque_materiais')
.insert(newMaterials)
.select();
if (error) throw error;
return { inserted: newMaterials.length, total: materiaisData.length };
},
onSuccess: (result) => {
queryClient.invalidateQueries({ queryKey: ['estoque-materiais'] });
toast.success(`${result.inserted} perfis W/HP cadastrados com sucesso!`);
},
onError: (error: any) => {
console.error('Erro ao popular perfis:', error);
toast.error(error.message || 'Erro ao cadastrar perfis W/HP');
},
});
};
+158
View File
@@ -0,0 +1,158 @@
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { useState, useMemo } from 'react';
export interface EstoqueMaterial {
id: string;
codigo: string;
descricao: string;
tipo_material_id?: string;
unidade: string;
quantidade_total: number;
quantidade_disponivel: number;
quantidade_empenhada: number;
quantidade_minima: number;
quantidade_maxima?: number;
peso_unitario: number;
valor_unitario?: number;
lote_atual?: string;
fornecedor?: string;
localizacao?: string;
status: 'Normal' | 'Crítico' | 'Excesso';
certificado?: string;
observacoes?: string;
comprimento?: number;
largura?: number;
espessura?: number;
qualidade_aco?: string;
kg_por_metro?: number;
created_at: string;
updated_at: string;
created_by?: string;
tipos_materia_prima?: {
nome: string;
descricao?: string;
categoria?: string;
gestao_estoque_critico?: boolean;
} | null;
}
export const useEstoqueMateriais = () => {
return useQuery({
queryKey: ['estoque-materiais'],
queryFn: async () => {
const { data, error } = await supabase
.from('estoque_materiais')
.select(`
*,
tipos_materia_prima (
nome,
descricao,
categoria,
gestao_estoque_critico
)
`)
.order('codigo');
if (error) throw error;
// Transform the raw data to match our EstoqueMaterial interface
const transformedData = (data || []).map(item => {
// Ensure tipos_materia_prima is properly typed
const tiposMaterial = item.tipos_materia_prima
? {
nome: item.tipos_materia_prima.nome || '',
descricao: item.tipos_materia_prima.descricao || undefined,
categoria: item.tipos_materia_prima.categoria || undefined,
gestao_estoque_critico: item.tipos_materia_prima.gestao_estoque_critico ?? true
}
: null;
return {
...item,
tipos_materia_prima: tiposMaterial
};
}) as EstoqueMaterial[];
return transformedData;
},
});
};
export const useEstoque = () => {
const [tiposSelecionados, setTiposSelecionados] = useState<string[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const { data: materiais = [], isLoading: loading, refetch } = useEstoqueMateriais();
// Recalcular status baseado na gestão de estoque crítico
const materiaisComStatusAtualizado = useMemo(() => {
return materiais.map(material => {
const temGestaoAtivada = material.tipos_materia_prima?.gestao_estoque_critico !== false;
let status = material.status;
// Se não tem gestão ativada, nunca pode ser crítico
if (!temGestaoAtivada && status === 'Crítico') {
if (material.quantidade_maxima && material.quantidade_disponivel >= material.quantidade_maxima) {
status = 'Excesso';
} else {
status = 'Normal';
}
}
// Se tem gestão ativada, recalcular normalmente
else if (temGestaoAtivada) {
if (material.quantidade_disponivel <= material.quantidade_minima && material.quantidade_minima > 0) {
status = 'Crítico';
} else if (material.quantidade_maxima && material.quantidade_disponivel >= material.quantidade_maxima) {
status = 'Excesso';
} else {
status = 'Normal';
}
}
return {
...material,
status
};
});
}, [materiais]);
// Calculate stats
const stats = useMemo(() => {
const totalKg = materiaisComStatusAtualizado.reduce((acc, material) => {
const kgPorMetro = material.kg_por_metro || 0;
const comprimento = material.comprimento || 0;
const quantidadeTotal = material.quantidade_total || 0;
const kgItem = (kgPorMetro * comprimento / 1000) * quantidadeTotal;
return acc + kgItem;
}, 0);
const totalUnidades = materiaisComStatusAtualizado.reduce((acc, material) => acc + (material.quantidade_total || 0), 0);
const totalDisponiveis = materiaisComStatusAtualizado.reduce((acc, material) => acc + (material.quantidade_disponivel || 0), 0);
const totalEmpenhadas = materiaisComStatusAtualizado.reduce((acc, material) => acc + (material.quantidade_empenhada || 0), 0);
return {
totalKg,
totalUnidades,
totalDisponiveis,
totalEmpenhadas
};
}, [materiaisComStatusAtualizado]);
const refreshData = () => {
refetch();
};
return {
materiais: materiaisComStatusAtualizado,
loading,
tiposSelecionados,
searchTerm,
setSearchTerm,
setTiposSelecionados,
stats,
refreshData
};
};
+453
View File
@@ -0,0 +1,453 @@
import { useState, useCallback } from 'react';
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { useAuth } from './useAuth';
import { toast } from 'sonner';
export interface FichaTecnicaData {
id?: string;
of_number: string;
gestor?: string;
projetista?: string;
revisao?: string;
quantidade?: number;
data_criacao?: string;
data_inicio?: string;
data_termino_prev?: string;
// Dados do Cliente
cliente?: string;
cnpj?: string;
ie?: string;
endereco?: string;
cidade?: string;
estado?: string;
cep?: string;
contato_contrato?: string;
fone_contrato?: string;
cel_contrato?: string;
email_contrato?: string;
contato_obra?: string;
fone_obra?: string;
cel_obra?: string;
email_obra?: string;
contato_qualid?: string;
fone_qualid?: string;
cel_qualid?: string;
email_qualid?: string;
// Dados da Obra
eng_responsavel?: string;
telefone_obra?: string;
email_obra_responsavel?: string;
endereco_obra?: string;
cep_obra?: string;
bairro_obra?: string;
cidade_obra?: string;
estado_obra?: string;
observacoes_obra?: string;
// Dados do Projeto
descricao_resumida?: string;
endereco_projeto?: string;
bairro_projeto?: string;
cep_projeto?: string;
cidade_projeto?: string;
estado_projeto?: string;
horarios_trabalho?: string;
condicoes_acesso?: string;
// Tipo de Projeto
tipo_estrutural?: boolean;
tipo_residencial?: boolean;
tipo_espacial?: boolean;
tipo_comercial?: boolean;
tipo_grades?: boolean;
tipo_industrial?: boolean;
tipo_cobertura?: boolean;
tipo_com_montagem?: boolean;
// Documentos fornecidos
doc_calculo?: boolean;
doc_projeto?: boolean;
doc_detalhamento?: boolean;
doc_cronograma?: boolean;
doc_normas?: boolean;
doc_especif_tecnicas?: boolean;
doc_catalogo?: boolean;
doc_fotos?: boolean;
// Informações do projeto
info_calculo_estrutural?: any;
info_projeto_basico?: any;
info_detalhamento?: any;
info_materia_prima?: any;
info_fabricacao?: any;
info_grades_piso?: any;
info_jateamento?: any;
info_pintura_base?: any;
info_pintura_inter?: any;
info_pintura_acabamento?: any;
info_galvanizacao?: any;
info_embalagem?: any;
info_transporte?: any;
info_inspecao?: any;
info_ensaios_lab?: any;
info_databook?: any;
info_pre_montagem?: any;
info_placa_engenetal?: any;
info_parafusos?: any;
info_chumbadores?: any;
info_stud_bolt?: any;
info_fornec_telhas?: any;
info_montagem_telhas?: any;
info_forn_calhas?: any;
info_mont_calhas?: any;
info_steel_deck?: any;
info_fornec_wall?: any;
info_mont_wall?: any;
info_outros_materiais?: any;
// Validação
necessita_validacao_pos_detalh?: boolean;
// Grades de piso
grades_modelo?: string;
grades_padrao_comercial?: boolean;
grades_padrao_sa2?: boolean;
grades_padrao_sa2_meio?: boolean;
grades_padrao_sa3?: boolean;
// Requisitos
req_ambientais_existem?: boolean;
req_ambientais_quais?: string;
req_saude_seguranca_existem?: boolean;
req_saude_seguranca_quais?: string;
// Alterações
alteracao_descritivo?: string;
alteracao_motivo?: string;
alteracao_impacto?: string;
alteracao_custo?: number;
alteracao_cronograma?: string;
alteracao_pecas_prontas?: string;
alteracao_detalh_projeto?: string;
// Cronograma
cronograma_semanas?: any;
// Vistos
visto_gestor?: string;
visto_pcp?: string;
visto_eng?: string;
visto_fab?: string;
visto_exp?: string;
visto_qual?: string;
visto_colunas?: any;
}
export function useFichaTecnica() {
const { user } = useAuth();
const [loading, setLoading] = useState(false);
const [fichaTecnica, setFichaTecnica] = useState<FichaTecnicaData | null>(null);
// Query to fetch all fichas técnicas for the user
const { data: fichasTecnicas, isLoading: isLoadingFichas } = useQuery({
queryKey: ['fichas-tecnicas', user?.id],
queryFn: async () => {
if (!user) return [];
const { data, error } = await supabase
.from('ficha_tecnica_contratos')
.select('*')
.eq('user_id', user.id)
.order('of_number', { ascending: true });
if (error) {
console.error('Erro ao buscar fichas técnicas:', error);
throw error;
}
return data as FichaTecnicaData[];
},
enabled: !!user
});
const buscarFichaTecnica = useCallback(async (ofNumber: string) => {
if (!user || !ofNumber.trim()) return;
setLoading(true);
try {
const { data, error } = await supabase
.from('ficha_tecnica_contratos')
.select('*')
.eq('of_number', ofNumber.trim())
.eq('user_id', user.id)
.maybeSingle();
if (error) {
console.error('Erro ao buscar ficha técnica:', error);
toast.error('Erro ao buscar ficha técnica');
return null;
}
console.log('Ficha técnica encontrada:', data);
setFichaTecnica(data);
return data;
} catch (error) {
console.error('Erro ao buscar ficha técnica:', error);
toast.error('Erro ao buscar ficha técnica');
return null;
} finally {
setLoading(false);
}
}, [user]);
const salvarFichaTecnica = useCallback(async (data: FichaTecnicaData) => {
if (!user) {
toast.error('Usuário não autenticado');
return false;
}
if (!data.of_number?.trim()) {
toast.error('Número da OF é obrigatório');
return false;
}
setLoading(true);
try {
console.log('Salvando ficha técnica com datas:', {
data_inicio: data.data_inicio,
data_termino_prev: data.data_termino_prev
});
// Criar objeto com APENAS os campos que existem no banco de dados
const dataToSave = {
// Campos básicos obrigatórios
of_number: data.of_number.trim(),
user_id: user.id,
updated_at: new Date().toISOString(),
// Informações básicas
gestor: data.gestor || null,
projetista: data.projetista || null,
revisao: data.revisao || '0',
quantidade: data.quantidade || null,
data_criacao: data.data_criacao || new Date().toISOString().split('T')[0],
data_inicio: data.data_inicio || null,
data_termino_prev: data.data_termino_prev || null,
// Dados do Cliente
cliente: data.cliente || null,
cnpj: data.cnpj || null,
ie: data.ie || null,
endereco: data.endereco || null,
cidade: data.cidade || null,
estado: data.estado || null,
cep: data.cep || null,
contato_contrato: data.contato_contrato || null,
fone_contrato: data.fone_contrato || null,
cel_contrato: data.cel_contrato || null,
email_contrato: data.email_contrato || null,
contato_obra: data.contato_obra || null,
fone_obra: data.fone_obra || null,
cel_obra: data.cel_obra || null,
email_obra: data.email_obra || null,
contato_qualid: data.contato_qualid || null,
fone_qualid: data.fone_qualid || null,
cel_qualid: data.cel_qualid || null,
email_qualid: data.email_qualid || null,
// Dados da Obra
eng_responsavel: data.eng_responsavel || null,
telefone_obra: data.telefone_obra || null,
email_obra_responsavel: data.email_obra_responsavel || null,
endereco_obra: data.endereco_obra || null,
cep_obra: data.cep_obra || null,
bairro_obra: data.bairro_obra || null,
cidade_obra: data.cidade_obra || null,
estado_obra: data.estado_obra || null,
observacoes_obra: data.observacoes_obra || null,
// Dados do Projeto
descricao_resumida: data.descricao_resumida || null,
endereco_projeto: data.endereco_projeto || null,
bairro_projeto: data.bairro_projeto || null,
cep_projeto: data.cep_projeto || null,
cidade_projeto: data.cidade_projeto || null,
estado_projeto: data.estado_projeto || null,
horarios_trabalho: data.horarios_trabalho || null,
condicoes_acesso: data.condicoes_acesso || null,
// Tipos de Projeto
tipo_estrutural: Boolean(data.tipo_estrutural),
tipo_residencial: Boolean(data.tipo_residencial),
tipo_espacial: Boolean(data.tipo_espacial),
tipo_comercial: Boolean(data.tipo_comercial),
tipo_grades: Boolean(data.tipo_grades),
tipo_industrial: Boolean(data.tipo_industrial),
tipo_cobertura: Boolean(data.tipo_cobertura),
tipo_com_montagem: Boolean(data.tipo_com_montagem),
// Documentos fornecidos
doc_calculo: Boolean(data.doc_calculo),
doc_projeto: Boolean(data.doc_projeto),
doc_detalhamento: Boolean(data.doc_detalhamento),
doc_cronograma: Boolean(data.doc_cronograma),
doc_normas: Boolean(data.doc_normas),
doc_especif_tecnicas: Boolean(data.doc_especif_tecnicas),
doc_catalogo: Boolean(data.doc_catalogo),
doc_fotos: Boolean(data.doc_fotos),
// Informações técnicas (JSONB)
info_calculo_estrutural: data.info_calculo_estrutural || { e: false, c: false, na: false, info: '' },
info_projeto_basico: data.info_projeto_basico || { e: false, c: false, na: false, info: '' },
info_detalhamento: data.info_detalhamento || { e: false, c: false, na: false, info: '' },
info_materia_prima: data.info_materia_prima || { e: false, c: false, na: false, info: '' },
info_fabricacao: data.info_fabricacao || { e: false, c: false, na: false, info: '' },
info_grades_piso: data.info_grades_piso || { e: false, c: false, na: false, info: '' },
info_jateamento: data.info_jateamento || { e: false, c: false, na: false, info: '' },
info_pintura_base: data.info_pintura_base || { e: false, c: false, na: false, info: '' },
info_pintura_inter: data.info_pintura_inter || { e: false, c: false, na: false, info: '' },
info_pintura_acabamento: data.info_pintura_acabamento || { e: false, c: false, na: false, info: '' },
info_galvanizacao: data.info_galvanizacao || { e: false, c: false, na: false, info: '' },
info_embalagem: data.info_embalagem || { e: false, c: false, na: false, info: '' },
info_transporte: data.info_transporte || { e: false, c: false, na: false, info: '' },
info_inspecao: data.info_inspecao || { e: false, c: false, na: false, info: '' },
info_ensaios_lab: data.info_ensaios_lab || { e: false, c: false, na: false, info: '' },
info_databook: data.info_databook || { e: false, c: false, na: false, info: '' },
info_pre_montagem: data.info_pre_montagem || { e: false, c: false, na: false, info: '' },
info_placa_engenetal: data.info_placa_engenetal || { e: false, c: false, na: false, info: '' },
info_parafusos: data.info_parafusos || { e: false, c: false, na: false, info: '' },
info_chumbadores: data.info_chumbadores || { e: false, c: false, na: false, info: '' },
info_stud_bolt: data.info_stud_bolt || { e: false, c: false, na: false, info: '' },
info_fornec_telhas: data.info_fornec_telhas || { e: false, c: false, na: false, info: '' },
info_montagem_telhas: data.info_montagem_telhas || { e: false, c: false, na: false, info: '' },
info_forn_calhas: data.info_forn_calhas || { e: false, c: false, na: false, info: '' },
info_mont_calhas: data.info_mont_calhas || { e: false, c: false, na: false, info: '' },
info_steel_deck: data.info_steel_deck || { e: false, c: false, na: false, info: '' },
info_fornec_wall: data.info_fornec_wall || { e: false, c: false, na: false, info: '' },
info_mont_wall: data.info_mont_wall || { e: false, c: false, na: false, info: '' },
info_outros_materiais: data.info_outros_materiais || { e: false, c: false, na: false, info: '' },
// Validação
necessita_validacao_pos_detalh: Boolean(data.necessita_validacao_pos_detalh),
// Grades
grades_modelo: data.grades_modelo || null,
grades_padrao_comercial: Boolean(data.grades_padrao_comercial),
grades_padrao_sa2: Boolean(data.grades_padrao_sa2),
grades_padrao_sa2_meio: Boolean(data.grades_padrao_sa2_meio),
grades_padrao_sa3: Boolean(data.grades_padrao_sa3),
// Requisitos
req_ambientais_existem: Boolean(data.req_ambientais_existem),
req_ambientais_quais: data.req_ambientais_quais || null,
req_saude_seguranca_existem: Boolean(data.req_saude_seguranca_existem),
req_saude_seguranca_quais: data.req_saude_seguranca_quais || null,
// Alterações
alteracao_descritivo: data.alteracao_descritivo || null,
alteracao_motivo: data.alteracao_motivo || null,
alteracao_impacto: data.alteracao_impacto || null,
alteracao_custo: data.alteracao_custo || null,
alteracao_cronograma: data.alteracao_cronograma || null,
alteracao_pecas_prontas: data.alteracao_pecas_prontas || null,
alteracao_detalh_projeto: data.alteracao_detalh_projeto || null,
// Cronograma e Vistos (JSONB)
cronograma_semanas: data.cronograma_semanas || {},
// Vistos
visto_gestor: data.visto_gestor || null,
visto_pcp: data.visto_pcp || null,
visto_eng: data.visto_eng || null,
visto_fab: data.visto_fab || null,
visto_exp: data.visto_exp || null,
visto_qual: data.visto_qual || null,
visto_colunas: data.visto_colunas || { '1': false, '2': false, '3': false, '4': false }
};
if (data.id) {
// Atualizar registro existente
const { error } = await supabase
.from('ficha_tecnica_contratos')
.update(dataToSave)
.eq('id', data.id)
.eq('user_id', user.id);
if (error) {
console.error('Erro ao atualizar ficha técnica:', error);
toast.error(`Erro ao atualizar ficha técnica: ${error.message}`);
return false;
}
toast.success('Ficha técnica atualizada com sucesso!');
} else {
// Verificar se já existe uma ficha para esta OF
const { data: existingFicha } = await supabase
.from('ficha_tecnica_contratos')
.select('id')
.eq('of_number', data.of_number.trim())
.eq('user_id', user.id)
.maybeSingle();
if (existingFicha) {
// Atualizar a ficha existente
const { error } = await supabase
.from('ficha_tecnica_contratos')
.update(dataToSave)
.eq('id', existingFicha.id)
.eq('user_id', user.id);
if (error) {
console.error('Erro ao atualizar ficha técnica existente:', error);
toast.error(`Erro ao atualizar ficha técnica: ${error.message}`);
return false;
}
toast.success('Ficha técnica atualizada com sucesso!');
} else {
// Criar nova ficha
const { data: newData, error } = await supabase
.from('ficha_tecnica_contratos')
.insert([dataToSave])
.select()
.single();
if (error) {
console.error('Erro ao criar ficha técnica:', error);
toast.error(`Erro ao criar ficha técnica: ${error.message}`);
return false;
}
setFichaTecnica(newData);
toast.success('Ficha técnica criada com sucesso!');
}
}
return true;
} catch (error) {
console.error('Erro inesperado ao salvar ficha técnica:', error);
toast.error('Erro inesperado ao salvar ficha técnica');
return false;
} finally {
setLoading(false);
}
}, [user]);
const limparFichaTecnica = useCallback(() => {
setFichaTecnica(null);
}, []);
return {
fichaTecnica,
fichasTecnicas,
loading,
isLoadingFichas,
buscarFichaTecnica,
salvarFichaTecnica,
limparFichaTecnica,
setFichaTecnica
};
}
+41
View File
@@ -0,0 +1,41 @@
import { useState, useEffect } from 'react';
interface FilterMemory {
selectedOF: string;
selectedFase: string;
}
const FILTER_MEMORY_KEY = 'prioridades-fabricacao-filters';
export const useFilterMemory = () => {
const [filters, setFilters] = useState<FilterMemory>({
selectedOF: '',
selectedFase: ''
});
// Carregar filtros salvos na inicialização
useEffect(() => {
const savedFilters = localStorage.getItem(FILTER_MEMORY_KEY);
if (savedFilters) {
try {
const parsedFilters = JSON.parse(savedFilters);
setFilters(parsedFilters);
} catch (error) {
console.log('Erro ao carregar filtros salvos:', error);
}
}
}, []);
// Salvar filtros sempre que mudarem
const updateFilters = (newFilters: Partial<FilterMemory>) => {
const updatedFilters = { ...filters, ...newFilters };
setFilters(updatedFilters);
localStorage.setItem(FILTER_MEMORY_KEY, JSON.stringify(updatedFilters));
};
return {
filters,
updateFilters
};
};
+58
View File
@@ -0,0 +1,58 @@
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
export type IconStyleType = 'white' | 'themed' | 'colorful';
interface IconStyleContextType {
iconStyle: IconStyleType;
setIconStyle: (style: IconStyleType) => void;
}
const IconStyleContext = createContext<IconStyleContextType | undefined>(undefined);
interface IconStyleProviderProps {
children: ReactNode;
}
export function IconStyleProvider({ children }: IconStyleProviderProps) {
const [iconStyle, setIconStyle] = useState<IconStyleType>(() => {
const saved = localStorage.getItem('icon-style') as IconStyleType;
return saved || 'colorful'; // Default to colorful for the new design
});
const handleSetIconStyle = (style: IconStyleType) => {
setIconStyle(style);
localStorage.setItem('icon-style', style);
};
useEffect(() => {
// Apply icon style to document root
const root = document.documentElement;
root.setAttribute('data-icon-style', iconStyle);
// Remove previous icon style classes
root.classList.remove('icon-style-white', 'icon-style-themed', 'icon-style-colorful');
// Add current icon style class
root.classList.add(`icon-style-${iconStyle}`);
}, [iconStyle]);
const value = {
iconStyle,
setIconStyle: handleSetIconStyle,
};
return (
<IconStyleContext.Provider value={value}>
{children}
</IconStyleContext.Provider>
);
}
export const useIconStyle = () => {
const context = useContext(IconStyleContext);
if (context === undefined) {
throw new Error('useIconStyle must be used within an IconStyleProvider');
}
return context;
};
+253
View File
@@ -0,0 +1,253 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
export interface InterfaceResource {
id: string;
resource_key: string;
resource_name: string;
parent_key: string | null;
icon_name: string | null;
route_path: string | null;
is_submenu: boolean;
order_index: number;
created_at: string;
updated_at: string;
}
export interface PrivilegeInterfaceResource {
id: string;
privilege_id: string;
resource_key: string;
created_at: string;
}
export function useInterfaceResources() {
const [resources, setResources] = useState<InterfaceResource[]>([]);
const [loading, setLoading] = useState(true);
// Fetch all interface resources (garantindo unicidade)
const fetchResources = async () => {
try {
const { data, error } = await supabase
.from('interface_resources')
.select('*')
.order('order_index');
if (error) throw error;
// Garantir que não há duplicatas por resource_key
const uniqueResourcesMap = new Map();
data?.forEach(resource => {
if (!uniqueResourcesMap.has(resource.resource_key)) {
uniqueResourcesMap.set(resource.resource_key, resource);
}
});
const uniqueResources = Array.from(uniqueResourcesMap.values());
setResources(uniqueResources);
console.log(`Carregados ${uniqueResources.length} recursos únicos`);
} catch (error) {
console.error('Error fetching interface resources:', error);
toast.error('Erro ao carregar recursos de interface');
}
};
// Get privilege interface resources
const getPrivilegeResources = async (privilegeId: string): Promise<string[]> => {
try {
console.log(`Loading resources for privilege ID: ${privilegeId}`);
const { data, error } = await supabase
.from('privilege_interface_resources')
.select('resource_key')
.eq('privilege_id', privilegeId);
if (error) {
console.error('Error fetching privilege resources:', error);
throw error;
}
// Remover duplicatas dos recursos retornados
const uniqueKeys = [...new Set(data?.map(item => item.resource_key) || [])];
console.log(`Found ${uniqueKeys.length} unique resources for privilege:`, uniqueKeys);
return uniqueKeys;
} catch (error) {
console.error('Error fetching privilege resources:', error);
return [];
}
};
// Update privilege interface resources
const updatePrivilegeResources = async (privilegeId: string, resourceKeys: string[]) => {
try {
console.log(`Updating privilege ${privilegeId} with resources:`, resourceKeys);
// First, delete existing associations
const { error: deleteError } = await supabase
.from('privilege_interface_resources')
.delete()
.eq('privilege_id', privilegeId);
if (deleteError) {
console.error('Error deleting existing resources:', deleteError);
throw deleteError;
}
// Then, insert new associations (sem duplicatas)
if (resourceKeys.length > 0) {
const uniqueKeys = [...new Set(resourceKeys)]; // Remove duplicatas
const insertData = uniqueKeys.map(resourceKey => ({
privilege_id: privilegeId,
resource_key: resourceKey
}));
console.log('Inserting privilege resources:', insertData);
const { error: insertError } = await supabase
.from('privilege_interface_resources')
.insert(insertData);
if (insertError) {
console.error('Error inserting privilege resources:', insertError);
throw insertError;
}
console.log(`Successfully updated ${uniqueKeys.length} resources for privilege ${privilegeId}`);
} else {
console.log(`No resources to insert for privilege ${privilegeId}`);
}
toast.success('Recursos de interface atualizados com sucesso!');
} catch (error) {
console.error('Error updating privilege resources:', error);
toast.error('Erro ao atualizar recursos de interface');
throw error;
}
};
// Check if user has access to a specific resource
const checkUserAccess = async (userId: string, resourceKey: string): Promise<boolean> => {
try {
const { data, error } = await supabase
.rpc('user_has_interface_access', {
_user_id: userId,
_resource_key: resourceKey
});
if (error) throw error;
return data || false;
} catch (error) {
console.error('Error checking user access:', error);
return false;
}
};
// Get user accessible resources
const getUserAccessibleResources = async (userId: string): Promise<string[]> => {
try {
// Primeiro verifica se o usuário é admin
const { data: userRoles, error: rolesError } = await supabase
.from('user_roles')
.select('role')
.eq('user_id', userId);
if (rolesError) throw rolesError;
const roles = userRoles?.map(r => r.role) || [];
// Se é admin, retorna todos os recursos únicos (lista fixa)
if (roles.includes('admin')) {
return [
'dashboard',
'cadastro',
'cadastro-of',
'cadastro-pecas',
'equipamentos',
'ferramentas',
'ferramentas-conversores',
'ferramentas-inconsistencias',
'estoque',
'estoque-solicitacao-compras',
'ofs',
'ofs-lista',
'ofs-cronograma',
'ofs-concluidas',
'producao',
'producao-visao',
'diario-producao',
'producao-apontamento',
'producao-dashboard',
'prioridades-fabricacao',
'painel-industrial',
'expedicao',
'obra',
'obra-dashboard',
'obra-configuracoes',
'tarefas',
'tarefas-lista',
'tarefas-historico',
'biblioteca',
'biblioteca-catalogos',
'biblioteca-normas',
'biblioteca-referencias',
'sistema',
'sugestoes',
'atribuicoes',
'mapa-interativo',
'configuracoes',
'configuracoes-gerais',
'theme-customization',
'admin',
'user-management'
];
}
// Para usuários não-admin, busca os recursos baseados no privilégio
const { data: profile, error: profileError } = await supabase
.from('profiles')
.select('privilege_id')
.eq('id', userId)
.single();
if (profileError) throw profileError;
if (!profile?.privilege_id) return [];
const { data, error } = await supabase
.from('privilege_interface_resources')
.select('resource_key')
.eq('privilege_id', profile.privilege_id);
if (error) throw error;
// Remover duplicatas dos recursos retornados
const uniqueKeys = [...new Set(data?.map(item => item.resource_key) || [])];
return uniqueKeys;
} catch (error) {
console.error('Error getting user accessible resources:', error);
return [];
}
};
useEffect(() => {
const loadResources = async () => {
setLoading(true);
await fetchResources();
setLoading(false);
};
loadResources();
}, []);
return {
resources,
loading,
fetchResources,
getPrivilegeResources,
updatePrivilegeResources,
checkUserAccess,
getUserAccessibleResources
};
}
+203
View File
@@ -0,0 +1,203 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
import { ItemPrioridade } from './usePrioridadesFabricacao';
export const useItensPrioridade = (prioridadeId?: string) => {
const [itens, setItens] = useState<ItemPrioridade[]>([]);
const [loading, setLoading] = useState(false);
const fetchItens = async (prioridadeId: string) => {
try {
setLoading(true);
const { data, error } = await supabase
.from('itens_prioridade_fabricacao')
.select(`
*,
peca:pecas(marca, descricao, peso_unitario, quantidade)
`)
.eq('prioridade_fabricacao_id', prioridadeId)
.order('ordem_fabricacao', { ascending: true });
if (error) {
console.error('Erro ao buscar itens:', error);
return;
}
setItens(data || []);
} catch (error) {
console.error('Erro ao buscar itens:', error);
} finally {
setLoading(false);
}
};
const adicionarItem = async (dadosItem: Omit<ItemPrioridade, 'id' | 'created_at' | 'updated_at'>) => {
try {
// Buscar próxima ordem
const { data: ultimoItem } = await supabase
.from('itens_prioridade_fabricacao')
.select('ordem_fabricacao')
.eq('prioridade_fabricacao_id', dadosItem.prioridade_fabricacao_id)
.order('ordem_fabricacao', { ascending: false })
.limit(1)
.single();
const proximaOrdem = ultimoItem ? ultimoItem.ordem_fabricacao + 1 : 1;
const { data, error } = await supabase
.from('itens_prioridade_fabricacao')
.insert([{
...dadosItem,
ordem_fabricacao: proximaOrdem
}])
.select()
.single();
if (error) {
console.error('Erro ao adicionar item:', error);
toast.error('Erro ao adicionar peça');
return null;
}
toast.success('Peça adicionada com sucesso!');
if (prioridadeId) {
await fetchItens(prioridadeId);
}
return data;
} catch (error) {
console.error('Erro ao adicionar item:', error);
toast.error('Erro ao adicionar peça');
return null;
}
};
const atualizarOrdemItens = async (itensOrdenados: { id: string; ordem_fabricacao: number }[]) => {
try {
const updates = itensOrdenados.map(item =>
supabase
.from('itens_prioridade_fabricacao')
.update({ ordem_fabricacao: item.ordem_fabricacao })
.eq('id', item.id)
);
await Promise.all(updates);
if (prioridadeId) {
await fetchItens(prioridadeId);
}
return true;
} catch (error) {
console.error('Erro ao atualizar ordem:', error);
toast.error('Erro ao atualizar ordem');
return false;
}
};
const removerItem = async (itemId: string) => {
try {
const { error } = await supabase
.from('itens_prioridade_fabricacao')
.delete()
.eq('id', itemId);
if (error) {
console.error('Erro ao remover item:', error);
toast.error('Erro ao remover peça');
return false;
}
toast.success('Peça removida com sucesso!');
if (prioridadeId) {
await fetchItens(prioridadeId);
}
return true;
} catch (error) {
console.error('Erro ao remover item:', error);
toast.error('Erro ao remover peça');
return false;
}
};
const atualizarQuantidade = async (itemId: string, novaQuantidade: number, pesoUnitario: number) => {
try {
const { error } = await supabase
.from('itens_prioridade_fabricacao')
.update({
quantidade_priorizada: novaQuantidade,
peso_total: novaQuantidade * pesoUnitario
})
.eq('id', itemId);
if (error) {
console.error('Erro ao atualizar quantidade:', error);
toast.error('Erro ao atualizar quantidade');
return false;
}
if (prioridadeId) {
await fetchItens(prioridadeId);
}
return true;
} catch (error) {
console.error('Erro ao atualizar quantidade:', error);
toast.error('Erro ao atualizar quantidade');
return false;
}
};
const transferirItem = async (itemId: string, novaPrioridadeId: string) => {
try {
// Buscar próxima ordem na nova prioridade
const { data: ultimoItem } = await supabase
.from('itens_prioridade_fabricacao')
.select('ordem_fabricacao')
.eq('prioridade_fabricacao_id', novaPrioridadeId)
.order('ordem_fabricacao', { ascending: false })
.limit(1)
.single();
const proximaOrdem = ultimoItem ? ultimoItem.ordem_fabricacao + 1 : 1;
const { error } = await supabase
.from('itens_prioridade_fabricacao')
.update({
prioridade_fabricacao_id: novaPrioridadeId,
ordem_fabricacao: proximaOrdem
})
.eq('id', itemId);
if (error) {
console.error('Erro ao transferir item:', error);
toast.error('Erro ao transferir peça');
return false;
}
return true;
} catch (error) {
console.error('Erro ao transferir item:', error);
toast.error('Erro ao transferir peça');
return false;
}
};
useEffect(() => {
if (prioridadeId) {
fetchItens(prioridadeId);
}
}, [prioridadeId]);
return {
itens,
loading,
fetchItens,
adicionarItem,
atualizarOrdemItens,
removerItem,
atualizarQuantidade,
transferirItem,
refetch: () => prioridadeId ? fetchItens(prioridadeId) : Promise.resolve()
};
};
+485
View File
@@ -0,0 +1,485 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
export interface ItemPrioridade {
id: string;
peca_id: string;
prioridade_fabricacao_id: string;
quantidade_priorizada: number;
peso_total: number;
ordem_fabricacao: number;
peca?: {
marca: string;
descricao?: string;
peso_unitario: number;
quantidade: number;
tem_componentes?: boolean;
of_number?: string;
etapa_fase?: string;
};
prioridade_fabricacao?: {
of_number: string;
etapa_fase: string;
revisao?: number;
data_ultima_modificacao?: string;
modificado_por?: string;
prioridade_config?: {
codigo: string;
nome: string;
cor: string;
};
};
}
export const useItensPrioridadeFabricacao = () => {
const [itensPorPrioridade, setItensPorPrioridade] = useState<{ [key: string]: ItemPrioridade[] }>({});
const [loading, setLoading] = useState(true);
const fetchItens = async () => {
try {
setLoading(true);
const { data, error } = await supabase
.from('itens_prioridade_fabricacao')
.select(`
*,
peca:pecas(marca, descricao, peso_unitario, quantidade, tem_componentes, of_number, etapa_fase),
prioridade_fabricacao:prioridades_fabricacao(
of_number,
etapa_fase,
revisao,
data_ultima_modificacao,
modificado_por,
prioridade_config:prioridades_config(codigo, nome, cor)
)
`)
.order('ordem_fabricacao', { ascending: true });
if (error) {
console.error('Erro ao buscar itens:', error);
toast.error('Erro ao carregar itens de prioridade');
return;
}
// Organizar itens por código de prioridade
const itensPorCodigo: { [key: string]: ItemPrioridade[] } = {};
data?.forEach((item) => {
const codigo = item.prioridade_fabricacao?.prioridade_config?.codigo || 'P4';
if (!itensPorCodigo[codigo]) {
itensPorCodigo[codigo] = [];
}
itensPorCodigo[codigo].push(item as ItemPrioridade);
});
setItensPorPrioridade(itensPorCodigo);
} catch (error) {
console.error('Erro ao buscar itens:', error);
toast.error('Erro ao carregar itens de prioridade');
} finally {
setLoading(false);
}
};
const atualizarQuantidade = async (itemId: string, novaQuantidade: number, pesoUnitario: number): Promise<boolean> => {
try {
const pesoTotal = novaQuantidade * pesoUnitario;
const { error } = await supabase
.from('itens_prioridade_fabricacao')
.update({
quantidade_priorizada: novaQuantidade,
peso_total: pesoTotal
})
.eq('id', itemId);
if (error) {
console.error('Erro ao atualizar quantidade:', error);
// Se o erro for de validação (função SQL), mostrar mensagem mais amigável
if (error.message.includes('excede o disponível')) {
toast.error('Quantidade solicitada excede o disponível para esta peça');
} else {
toast.error('Erro ao atualizar quantidade');
}
return false;
}
await fetchItens();
toast.success('Quantidade atualizada!');
return true;
} catch (error) {
console.error('Erro ao atualizar quantidade:', error);
toast.error('Erro ao atualizar quantidade');
return false;
}
};
const removerItem = async (itemId: string): Promise<boolean> => {
try {
console.log('🗑️ Iniciando remoção do item:', itemId);
// Buscar o item com todos os dados necessários incluindo OF e fase
const { data: itemParaRemover, error: errorBuscar } = await supabase
.from('itens_prioridade_fabricacao')
.select(`
*,
peca:pecas(of_number, etapa_fase, marca),
prioridade_fabricacao:prioridades_fabricacao(of_number, etapa_fase)
`)
.eq('id', itemId)
.single();
if (errorBuscar || !itemParaRemover) {
console.error('❌ Erro ao buscar item para remoção:', errorBuscar);
toast.error('Erro: Item não encontrado');
return false;
}
const ofNumber = itemParaRemover.peca?.of_number || itemParaRemover.prioridade_fabricacao?.of_number;
const etapaFase = itemParaRemover.peca?.etapa_fase || itemParaRemover.prioridade_fabricacao?.etapa_fase;
if (!ofNumber || !etapaFase) {
console.error('❌ Dados de OF ou etapa/fase não encontrados:', { ofNumber, etapaFase });
toast.error('Erro: Dados da OF ou etapa/fase não encontrados');
return false;
}
console.log('📋 Validando remoção para OF:', ofNumber, 'Etapa/Fase:', etapaFase);
// Remover o item com validação adicional de OF e fase
const { error } = await supabase
.from('itens_prioridade_fabricacao')
.delete()
.eq('id', itemId)
.eq('prioridade_fabricacao_id', itemParaRemover.prioridade_fabricacao_id);
if (error) {
console.error('❌ Erro ao remover item:', error);
toast.error('Erro ao remover item');
return false;
}
console.log('✅ Item removido com sucesso da OF:', ofNumber, 'Etapa/Fase:', etapaFase);
await fetchItens();
toast.success('Peça removida da prioridade!');
return true;
} catch (error) {
console.error('❌ Erro ao remover item:', error);
toast.error('Erro ao remover item');
return false;
}
};
const removerItensPorPrioridade = async (codigoPrioridade: string): Promise<{ sucessos: number; falhas: number }> => {
try {
console.log('🗑️ Iniciando remoção em lote para prioridade:', codigoPrioridade);
const itens = itensPorPrioridade[codigoPrioridade] || [];
if (itens.length === 0) {
console.log('⚠️ Nenhum item para remover na prioridade:', codigoPrioridade);
return { sucessos: 0, falhas: 0 };
}
// Agrupar itens por OF e etapa/fase para validação
const itensAgrupados: { [key: string]: ItemPrioridade[] } = {};
itens.forEach(item => {
const ofNumber = item.peca?.of_number || item.prioridade_fabricacao?.of_number;
const etapaFase = item.peca?.etapa_fase || item.prioridade_fabricacao?.etapa_fase;
const chave = `${ofNumber}-${etapaFase}`;
if (!itensAgrupados[chave]) {
itensAgrupados[chave] = [];
}
itensAgrupados[chave].push(item);
});
console.log('📊 Itens agrupados por OF/Etapa:', Object.keys(itensAgrupados));
let sucessos = 0;
let falhas = 0;
// Processar cada grupo de OF/Etapa separadamente
for (const [chaveGrupo, itensGrupo] of Object.entries(itensAgrupados)) {
const [ofNumber, etapaFase] = chaveGrupo.split('-');
console.log(`🔄 Processando grupo: OF ${ofNumber}, Etapa/Fase ${etapaFase} (${itensGrupo.length} itens)`);
// Buscar IDs dos itens deste grupo para validação adicional
const idsParaRemover = itensGrupo.map(item => item.id);
const { data: itensValidados, error: errorValidacao } = await supabase
.from('itens_prioridade_fabricacao')
.select(`
id,
prioridade_fabricacao_id,
peca:pecas(of_number, etapa_fase, marca),
prioridade_fabricacao:prioridades_fabricacao(of_number, etapa_fase)
`)
.in('id', idsParaRemover);
if (errorValidacao) {
console.error('❌ Erro na validação dos itens:', errorValidacao);
falhas += itensGrupo.length;
continue;
}
// Filtrar apenas itens que realmente pertencem à OF e etapa/fase corretas
const itensValidadosParaRemover = itensValidados?.filter(item => {
const itemOfNumber = item.peca?.of_number || item.prioridade_fabricacao?.of_number;
const itemEtapaFase = item.peca?.etapa_fase || item.prioridade_fabricacao?.etapa_fase;
return itemOfNumber === ofNumber && itemEtapaFase === etapaFase;
}) || [];
if (itensValidadosParaRemover.length === 0) {
console.log(`⚠️ Nenhum item validado para remoção no grupo ${chaveGrupo}`);
falhas += itensGrupo.length;
continue;
}
// Remover itens validados
const { error: errorRemocao } = await supabase
.from('itens_prioridade_fabricacao')
.delete()
.in('id', itensValidadosParaRemover.map(item => item.id));
if (errorRemocao) {
console.error(`❌ Erro ao remover itens do grupo ${chaveGrupo}:`, errorRemocao);
falhas += itensGrupo.length;
} else {
console.log(`${itensValidadosParaRemover.length} itens removidos do grupo ${chaveGrupo}`);
sucessos += itensValidadosParaRemover.length;
}
}
console.log(`📊 Resultado da remoção em lote: ${sucessos} sucessos, ${falhas} falhas`);
if (sucessos > 0) {
await fetchItens();
}
return { sucessos, falhas };
} catch (error) {
console.error('❌ Erro na remoção em lote:', error);
const totalItens = itensPorPrioridade[codigoPrioridade]?.length || 0;
return { sucessos: 0, falhas: totalItens };
}
};
const buscarOuCriarPrioridadeFabricacao = async (ofNumber: string, etapaFase: string, codigoPrioridade: string): Promise<string | null> => {
try {
console.log('🔍 Buscando prioridade fabricação para:', { ofNumber, etapaFase, codigoPrioridade });
// Primeiro, buscar a configuração de prioridade pelo código
const { data: prioridadeConfig, error: errorConfig } = await supabase
.from('prioridades_config')
.select('id, nome')
.eq('codigo', codigoPrioridade)
.single();
if (errorConfig || !prioridadeConfig) {
console.error('❌ Erro ao buscar configuração de prioridade:', errorConfig);
toast.error('Configuração de prioridade não encontrada');
return null;
}
console.log('✅ Configuração de prioridade encontrada:', prioridadeConfig);
// Buscar prioridade_fabricacao existente
const { data: prioridadeExistente, error: errorExistente } = await supabase
.from('prioridades_fabricacao')
.select('id')
.eq('of_number', ofNumber)
.eq('etapa_fase', etapaFase)
.eq('prioridade_id', prioridadeConfig.id)
.single();
if (prioridadeExistente && !errorExistente) {
console.log('✅ Prioridade fabricação existente encontrada:', prioridadeExistente.id);
return prioridadeExistente.id;
}
// Se não existe, criar nova prioridade_fabricacao
console.log('🔨 Criando nova prioridade fabricação...');
const { data: novaPrioridade, error: errorNova } = await supabase
.from('prioridades_fabricacao')
.insert({
of_number: ofNumber,
etapa_fase: etapaFase,
prioridade_id: prioridadeConfig.id,
nome_prioridade: prioridadeConfig.nome,
ativo: true,
revisao: 0
})
.select('id')
.single();
if (errorNova || !novaPrioridade) {
console.error('❌ Erro ao criar prioridade de fabricação:', errorNova);
toast.error('Erro ao criar prioridade de fabricação');
return null;
}
console.log('✅ Nova prioridade fabricação criada:', novaPrioridade.id);
return novaPrioridade.id;
} catch (error) {
console.error('❌ Erro ao buscar/criar prioridade de fabricação:', error);
toast.error('Erro ao processar prioridade de fabricação');
return null;
}
};
const transferirItem = async (itemId: string, codigoPrioridadeDestino: string): Promise<boolean> => {
try {
console.log('🔄 Iniciando transferência do item:', itemId, 'para prioridade:', codigoPrioridadeDestino);
// Buscar o item atual com todos os dados necessários
const { data: itemAtual, error: errorItem } = await supabase
.from('itens_prioridade_fabricacao')
.select(`
*,
prioridade_fabricacao:prioridades_fabricacao(
of_number,
etapa_fase
)
`)
.eq('id', itemId)
.single();
if (errorItem || !itemAtual) {
console.error('❌ Erro ao buscar item atual:', errorItem);
toast.error('Erro ao buscar item para transferência');
return false;
}
console.log('✅ Item atual encontrado:', itemAtual);
// Verificar se a prioridade_fabricacao existe e tem os dados necessários
if (!itemAtual.prioridade_fabricacao) {
console.error('❌ Prioridade de fabricação não encontrada no item');
toast.error('Prioridade de fabricação não encontrada no item');
return false;
}
// Type assertion para garantir que temos o tipo correto
const prioridadeFab = itemAtual.prioridade_fabricacao as { of_number: string; etapa_fase: string };
if (!prioridadeFab.of_number || !prioridadeFab.etapa_fase) {
console.error('❌ Dados da prioridade de fabricação incompletos:', prioridadeFab);
toast.error('Dados da prioridade de fabricação incompletos');
return false;
}
const { of_number, etapa_fase } = prioridadeFab;
console.log('📋 Dados extraídos:', { of_number, etapa_fase });
// Buscar ou criar prioridade_fabricacao de destino
const prioridadeFabricacaoDestinoId = await buscarOuCriarPrioridadeFabricacao(
of_number,
etapa_fase,
codigoPrioridadeDestino
);
if (!prioridadeFabricacaoDestinoId) {
console.error('❌ Falha ao obter ID da prioridade de fabricação de destino');
return false;
}
console.log('🎯 Prioridade de destino obtida:', prioridadeFabricacaoDestinoId);
// Buscar a maior ordem_fabricacao existente na prioridade de destino
const { data: itensDestino, error: errorItensDestino } = await supabase
.from('itens_prioridade_fabricacao')
.select('ordem_fabricacao')
.eq('prioridade_fabricacao_id', prioridadeFabricacaoDestinoId)
.order('ordem_fabricacao', { ascending: false })
.limit(1);
if (errorItensDestino) {
console.error('❌ Erro ao buscar itens da prioridade de destino:', errorItensDestino);
toast.error('Erro ao buscar itens da prioridade de destino');
return false;
}
// Calcular a nova ordem_fabricacao (final da lista)
const novaOrdem = itensDestino && itensDestino.length > 0 ? itensDestino[0].ordem_fabricacao + 1 : 1;
console.log('🔢 Nova ordem calculada:', novaOrdem);
// Transferir o item com a nova ordem
const { error: errorTransferencia } = await supabase
.from('itens_prioridade_fabricacao')
.update({
prioridade_fabricacao_id: prioridadeFabricacaoDestinoId,
ordem_fabricacao: novaOrdem
})
.eq('id', itemId);
if (errorTransferencia) {
console.error('❌ Erro ao transferir item:', errorTransferencia);
// Se o erro for de validação (função SQL), mostrar mensagem mais amigável
if (errorTransferencia.message.includes('excede o disponível')) {
toast.error('Não é possível transferir: quantidade excede o disponível na prioridade de destino');
} else {
toast.error('Erro ao transferir item');
}
return false;
}
console.log('✅ Item transferido com sucesso para o final da lista');
await fetchItens();
toast.success('Item transferido com sucesso!');
return true;
} catch (error) {
console.error('❌ Erro ao transferir item:', error);
toast.error('Erro ao transferir item');
return false;
}
};
const reorderItems = async (prioridadeId: string, itensOrdenados: { id: string; ordem_fabricacao: number }[]): Promise<boolean> => {
try {
const updates = itensOrdenados.map(item =>
supabase
.from('itens_prioridade_fabricacao')
.update({ ordem_fabricacao: item.ordem_fabricacao })
.eq('id', item.id)
);
const results = await Promise.all(updates);
const hasError = results.some(result => result.error);
if (hasError) {
console.error('❌ Erro ao reordenar itens');
toast.error('Erro ao reordenar itens');
return false;
}
await fetchItens();
return true;
} catch (error) {
console.error('❌ Erro ao reordenar itens:', error);
toast.error('Erro ao reordenar itens');
return false;
}
};
useEffect(() => {
fetchItens();
}, []);
return {
itensPorPrioridade,
loading,
atualizarQuantidade,
removerItem,
removerItensPorPrioridade,
transferirItem,
reorderItems,
refetch: fetchItens,
buscarOuCriarPrioridadeFabricacao
};
};
@@ -0,0 +1,103 @@
import { useState, useEffect, useMemo } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
import { useItensPrioridadeFabricacao, ItemPrioridade } from './useItensPrioridadeFabricacao';
export const useItensPrioridadeFabricacaoFiltrado = () => {
const [ofSelecionada, setOfSelecionada] = useState<string | null>(null);
const [faseSelecionada, setFaseSelecionada] = useState<string | null>(null);
const [versaoAtual, setVersaoAtual] = useState<{
revisao: number;
dataModificacao: string;
modificadoPor?: string;
} | null>(null);
// Use o hook original
const hookOriginal = useItensPrioridadeFabricacao();
// Filtrar itens baseado nos filtros selecionados
const itensFiltrados = useMemo(() => {
if (!ofSelecionada || !faseSelecionada) {
return {};
}
const itensFiltradosObj: { [key: string]: ItemPrioridade[] } = {};
Object.entries(hookOriginal.itensPorPrioridade).forEach(([prioridade, itens]) => {
const itensDaPrioridadeFiltrados = itens.filter(item => {
const itemOf = item.peca?.of_number || item.prioridade_fabricacao?.of_number;
const itemFase = item.peca?.etapa_fase || item.prioridade_fabricacao?.etapa_fase;
return itemOf === ofSelecionada && itemFase === faseSelecionada;
});
if (itensDaPrioridadeFiltrados.length > 0) {
itensFiltradosObj[prioridade] = itensDaPrioridadeFiltrados;
}
});
return itensFiltradosObj;
}, [hookOriginal.itensPorPrioridade, ofSelecionada, faseSelecionada]);
// Buscar informações de versão quando filtros mudarem
useEffect(() => {
const buscarVersaoAtual = async () => {
if (!ofSelecionada || !faseSelecionada) {
setVersaoAtual(null);
return;
}
try {
const { data, error } = await supabase
.from('prioridades_fabricacao')
.select(`
revisao,
data_ultima_modificacao,
profiles:modificado_por(full_name)
`)
.eq('of_number', ofSelecionada)
.eq('etapa_fase', faseSelecionada)
.order('revisao', { ascending: false })
.limit(1)
.single();
if (error && error.code !== 'PGRST116') {
console.error('Erro ao buscar versão:', error);
return;
}
if (data) {
setVersaoAtual({
revisao: data.revisao || 0,
dataModificacao: data.data_ultima_modificacao || new Date().toISOString(),
modificadoPor: (data.profiles as any)?.full_name
});
} else {
setVersaoAtual({
revisao: 0,
dataModificacao: new Date().toISOString()
});
}
} catch (error) {
console.error('Erro ao buscar versão:', error);
}
};
buscarVersaoAtual();
}, [ofSelecionada, faseSelecionada]);
const handleFiltroChange = (novaOf: string | null, novaFase: string | null) => {
setOfSelecionada(novaOf);
setFaseSelecionada(novaFase);
};
return {
...hookOriginal,
itensPorPrioridade: itensFiltrados,
ofSelecionada,
faseSelecionada,
versaoAtual,
onFiltroChange: handleFiltroChange
};
};
+123
View File
@@ -0,0 +1,123 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
export interface JsonCode {
id: string;
name: string;
description?: string;
json_code: any;
is_active: boolean;
created_at: string;
updated_at: string;
created_by?: string;
}
export function useJsonCodes() {
const [jsonCodes, setJsonCodes] = useState<JsonCode[]>([]);
const [loading, setLoading] = useState(true);
const fetchJsonCodes = async () => {
try {
const { data, error } = await (supabase as any)
.from('json_codes')
.select('*')
.order('created_at', { ascending: false });
if (error) throw error;
setJsonCodes(data || []);
} catch (error) {
console.error('Erro ao carregar códigos JSON:', error);
toast.error('Erro ao carregar códigos JSON');
} finally {
setLoading(false);
}
};
const saveJsonCode = async (jsonCode: Partial<JsonCode>) => {
try {
if (jsonCode.id) {
// Atualizar código existente
const { error } = await (supabase as any)
.from('json_codes')
.update({
name: jsonCode.name,
description: jsonCode.description,
json_code: jsonCode.json_code,
is_active: jsonCode.is_active,
updated_at: new Date().toISOString()
})
.eq('id', jsonCode.id);
if (error) throw error;
} else {
// Criar novo código
const { error } = await (supabase as any)
.from('json_codes')
.insert({
name: jsonCode.name,
description: jsonCode.description,
json_code: jsonCode.json_code,
is_active: jsonCode.is_active ?? true,
created_by: (await supabase.auth.getUser()).data.user?.id
});
if (error) throw error;
}
await fetchJsonCodes();
toast.success('Código JSON salvo com sucesso!');
} catch (error) {
console.error('Erro ao salvar código JSON:', error);
toast.error('Erro ao salvar código JSON');
}
};
const deleteJsonCode = async (codeId: string) => {
try {
const { error } = await (supabase as any)
.from('json_codes')
.delete()
.eq('id', codeId);
if (error) throw error;
await fetchJsonCodes();
toast.success('Código JSON removido com sucesso!');
} catch (error) {
console.error('Erro ao remover código JSON:', error);
toast.error('Erro ao remover código JSON');
}
};
const toggleActiveStatus = async (codeId: string, isActive: boolean) => {
try {
const { error } = await (supabase as any)
.from('json_codes')
.update({ is_active: isActive, updated_at: new Date().toISOString() })
.eq('id', codeId);
if (error) throw error;
await fetchJsonCodes();
toast.success(`Código JSON ${isActive ? 'ativado' : 'desativado'} com sucesso!`);
} catch (error) {
console.error('Erro ao alterar status do código JSON:', error);
toast.error('Erro ao alterar status do código JSON');
}
};
useEffect(() => {
fetchJsonCodes();
}, []);
return {
jsonCodes,
loading,
saveJsonCode,
deleteJsonCode,
toggleActiveStatus,
refetch: fetchJsonCodes
};
}
+40
View File
@@ -0,0 +1,40 @@
import { useMemo } from 'react';
import { useEstoque } from './useEstoqueSimplificado';
export const useMateriaisCriticos = () => {
const { materiais, loading } = useEstoque();
const materiaisCriticos = useMemo(() => {
return materiais.filter(material => {
// Verificar se o tipo de material tem gestão de estoque crítico ativada
// Se gestao_estoque_critico for undefined ou null, considerar como true (padrão)
const temGestaoAtivada = material.tipos_materia_prima?.gestao_estoque_critico !== false;
// Debug log para verificar o filtro
console.log(`Material ${material.descricao}:`, {
gestao_estoque_critico: material.tipos_materia_prima?.gestao_estoque_critico,
temGestaoAtivada,
quantidade_disponivel: material.quantidade_disponivel,
quantidade_minima: material.quantidade_minima,
isCritico: material.quantidade_disponivel <= (material.quantidade_minima || 0) && material.quantidade_minima > 0
});
// Só considera crítico se:
// 1. Tem gestão de estoque crítico ativada no tipo
// 2. Quantidade disponível <= quantidade mínima
// 3. Quantidade mínima > 0
return temGestaoAtivada &&
material.quantidade_disponivel <= (material.quantidade_minima || 0) &&
(material.quantidade_minima || 0) > 0;
});
}, [materiais]);
console.log('Materiais críticos encontrados:', materiaisCriticos.length);
return {
materiaisCriticos,
loading,
totalCriticos: materiaisCriticos.length
};
};
+55
View File
@@ -0,0 +1,55 @@
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
export const useMateriaisEmSC = () => {
return useQuery({
queryKey: ['materiais-em-sc'],
queryFn: async () => {
// Buscar SCs não concluídas
const { data: solicitacoes, error: scError } = await supabase
.from('solicitacoes_compra')
.select(`
id,
numero_sc,
status,
itens_solicitacao_compra (
material_id,
quantidade,
estoque_materiais (
id,
codigo,
descricao
)
)
`)
.neq('status', 'Concluída');
if (scError) throw scError;
// Criar mapa de materiais em SCs não concluídas
const materiaisEmSC = new Map();
solicitacoes?.forEach(sc => {
sc.itens_solicitacao_compra?.forEach(item => {
if (item.material_id && item.estoque_materiais) {
const materialId = item.material_id;
if (!materiaisEmSC.has(materialId)) {
materiaisEmSC.set(materialId, {
material: item.estoque_materiais,
solicitacoes: []
});
}
materiaisEmSC.get(materialId).solicitacoes.push({
numero_sc: sc.numero_sc,
status: sc.status,
quantidade: item.quantidade
});
}
});
});
return materiaisEmSC;
},
});
};
+80
View File
@@ -0,0 +1,80 @@
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
export interface MaterialEstoque {
id: string;
descricao: string;
unidade: string;
tipo_material_id?: string;
tipo_material_nome?: string;
codigo: string;
quantidade_disponivel?: number;
peso_unitario?: number;
valor_unitario?: number;
}
export const useMateriaisEstoque = (filters?: {
tipo?: string;
busca?: string;
}) => {
const { data: materiais = [], isLoading } = useQuery({
queryKey: ['materiais-estoque', filters],
queryFn: async () => {
let query = supabase
.from('estoque_materiais')
.select(`
id,
descricao,
unidade,
tipo_material_id,
codigo,
quantidade_disponivel,
peso_unitario,
valor_unitario,
tipos_materia_prima!tipo_material_id(nome)
`)
.order('descricao');
if (filters?.tipo) {
// Buscar IDs dos tipos de material pelo nome
const { data: tiposData } = await supabase
.from('tipos_materia_prima')
.select('id')
.eq('nome', filters.tipo);
if (tiposData && tiposData.length > 0) {
query = query.eq('tipo_material_id', tiposData[0].id);
}
}
if (filters?.busca) {
query = query.ilike('descricao', `%${filters.busca}%`);
}
const { data, error } = await query;
if (error) {
console.error('Erro ao buscar materiais:', error);
throw error;
}
return (data || []).map(item => ({
id: item.id,
descricao: item.descricao,
unidade: item.unidade,
tipo_material_id: item.tipo_material_id,
tipo_material_nome: item.tipos_materia_prima?.nome || 'N/A',
codigo: item.codigo,
quantidade_disponivel: item.quantidade_disponivel,
peso_unitario: item.peso_unitario,
valor_unitario: item.valor_unitario
})) as MaterialEstoque[];
},
});
return {
materiais,
isLoading,
};
};
+82
View File
@@ -0,0 +1,82 @@
import { useState, useEffect } from 'react';
export interface MobileResponsiveConfig {
isMobile: boolean;
isTablet: boolean;
isDesktop: boolean;
isLandscape: boolean;
screenWidth: number;
screenHeight: number;
touchSupport: boolean;
}
export function useMobileResponsive(): MobileResponsiveConfig {
const [config, setConfig] = useState<MobileResponsiveConfig>({
isMobile: false,
isTablet: false,
isDesktop: false,
isLandscape: false,
screenWidth: 0,
screenHeight: 0,
touchSupport: false
});
useEffect(() => {
const updateConfig = () => {
const width = window.innerWidth;
const height = window.innerHeight;
setConfig({
isMobile: width < 768,
isTablet: width >= 768 && width < 1024,
isDesktop: width >= 1024,
isLandscape: width > height,
screenWidth: width,
screenHeight: height,
touchSupport: 'ontouchstart' in window || navigator.maxTouchPoints > 0
});
};
updateConfig();
window.addEventListener('resize', updateConfig);
window.addEventListener('orientationchange', updateConfig);
return () => {
window.removeEventListener('resize', updateConfig);
window.removeEventListener('orientationchange', updateConfig);
};
}, []);
return config;
}
// Utility functions for responsive design
export const getResponsiveClasses = (mobile: boolean, tablet: boolean) => {
return {
container: mobile ? 'px-2 py-4' : tablet ? 'px-4 py-6' : 'px-6 py-8',
grid: mobile ? 'grid-cols-1' : tablet ? 'grid-cols-2' : 'grid-cols-3',
spacing: mobile ? 'gap-3' : tablet ? 'gap-4' : 'gap-6',
text: mobile ? 'text-sm' : tablet ? 'text-base' : 'text-base',
button: mobile ? 'w-full min-h-[44px]' : 'w-auto',
card: mobile ? 'p-4' : tablet ? 'p-5' : 'p-6'
};
};
export const getTableResponsiveClasses = (mobile: boolean) => {
return {
container: mobile ? 'mobile-table-scroll' : '',
table: mobile ? 'min-w-full text-sm' : 'min-w-full',
cell: mobile ? 'px-2 py-3' : 'px-4 py-3',
header: mobile ? 'px-2 py-2 text-xs font-medium' : 'px-4 py-3 text-sm font-medium'
};
};
export const getModalResponsiveClasses = (mobile: boolean) => {
return {
content: mobile ? 'mobile-modal' : 'max-w-2xl',
header: mobile ? 'mobile-modal-header' : 'p-6 pb-4',
body: mobile ? 'mobile-modal-content' : 'px-6 py-4',
footer: mobile ? 'mobile-modal-footer' : 'p-6 pt-4 flex justify-end gap-3'
};
};
+134
View File
@@ -0,0 +1,134 @@
import { useState } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { useAuth } from './useAuth';
import { toast } from 'sonner';
export interface FichaTecnicaOption {
id: string;
of_number: string;
descritivo?: string;
cliente?: string;
data_inicio?: string;
data_termino_prev?: string;
peso_total?: number;
gestor?: string;
}
export interface NovaOFData {
of_number: string;
descritivo?: string;
data_abertura?: string;
data_prazo?: string;
data_termino_prev?: string;
peso_total?: number;
gestor?: string;
prioridade: string;
nivel_qualidade: string;
criterio_qualidade?: string;
tratamento_final?: string;
local_uf?: string;
status: string;
ficha_tecnica_id: string;
}
export const useNovaOF = () => {
const { user } = useAuth();
const [loading, setLoading] = useState(false);
const [fichasTecnicas, setFichasTecnicas] = useState<FichaTecnicaOption[]>([]);
const buscarFichasTecnicas = async () => {
if (!user) return;
setLoading(true);
try {
const { data, error } = await supabase
.from('ficha_tecnica_contratos')
.select('id, of_number, descricao_resumida, cliente, data_inicio, data_termino_prev, quantidade, gestor')
.eq('user_id', user.id)
.order('of_number');
if (error) throw error;
const fichasFormatadas = data.map(ficha => ({
id: ficha.id,
of_number: ficha.of_number,
descritivo: ficha.descricao_resumida,
cliente: ficha.cliente,
data_inicio: ficha.data_inicio,
data_termino_prev: ficha.data_termino_prev,
peso_total: ficha.quantidade || 0,
gestor: ficha.gestor
}));
setFichasTecnicas(fichasFormatadas);
} catch (error) {
console.error('Erro ao buscar fichas técnicas:', error);
toast.error('Erro ao carregar fichas técnicas');
} finally {
setLoading(false);
}
};
const criarNovaOF = async (data: NovaOFData) => {
if (!user) {
toast.error('Usuário não autenticado');
return false;
}
setLoading(true);
try {
// Verificar se já existe uma OF com este número
const { data: existingOF } = await supabase
.from('ordens_fabricacao')
.select('id')
.eq('num_of', data.of_number)
.maybeSingle();
if (existingOF) {
toast.error('Já existe uma Ordem de Fabricação com este número');
return false;
}
const novaOF = {
num_of: data.of_number,
descritivo: data.descritivo || null,
data_abertura: data.data_abertura || new Date().toISOString().split('T')[0],
data_prazo: data.data_prazo || null,
data_termino_prev: data.data_termino_prev || null,
peso_total: data.peso_total || null,
gestor: data.gestor || null,
prioridade: data.prioridade || 'Normal',
nivel_qualidade: data.nivel_qualidade || 'Normal',
criterio_qualidade: data.criterio_qualidade || null,
tratamento_final: data.tratamento_final || null,
local_uf: data.local_uf || null,
status: data.status || 'ativa',
user_id: user.id,
ficha_tecnica_id: data.ficha_tecnica_id
};
const { error } = await supabase
.from('ordens_fabricacao')
.insert([novaOF]);
if (error) throw error;
toast.success('Ordem de Fabricação criada com sucesso!');
return true;
} catch (error) {
console.error('Erro ao criar OF:', error);
toast.error('Erro ao criar Ordem de Fabricação');
return false;
} finally {
setLoading(false);
}
};
return {
loading,
fichasTecnicas,
buscarFichasTecnicas,
criarNovaOF
};
};
+77
View File
@@ -0,0 +1,77 @@
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
export interface OrdemFabricacao {
id: string;
num_of: string;
descritivo?: string;
status?: string;
data_abertura?: string;
data_prazo?: string;
data_termino_prev?: string;
peso_total?: number;
gestor?: string;
prioridade?: string;
nivel_qualidade?: string;
criterio_qualidade?: string;
tratamento_final?: string;
local_uf?: string;
user_id?: string;
ficha_tecnica_id?: string;
}
export const useOFs = () => {
const query = useQuery({
queryKey: ['ordens-fabricacao'],
queryFn: async () => {
const { data, error } = await supabase
.from('ordens_fabricacao')
.select('*')
.eq('status', 'ativa')
.order('num_of');
if (error) throw error;
return data as OrdemFabricacao[];
},
});
const updateOF = async (id: string, updates: Partial<OrdemFabricacao>) => {
const { error } = await supabase
.from('ordens_fabricacao')
.update(updates)
.eq('id', id);
if (error) throw error;
query.refetch();
};
const deleteOF = async (id: string) => {
const { error } = await supabase
.from('ordens_fabricacao')
.delete()
.eq('id', id);
if (error) throw error;
query.refetch();
};
const concluirOF = async (ofId: string) => {
const { error } = await supabase.rpc('concluir_of', {
of_id_param: ofId
});
if (error) throw error;
query.refetch();
};
return {
...query,
ofs: query.data || [],
loading: query.isLoading,
refetch: query.refetch,
updateOF,
deleteOF,
concluirOF
};
};
+36
View File
@@ -0,0 +1,36 @@
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
export interface OFAtiva {
of_number: string;
descricao_resumida?: string;
cliente?: string;
gestor?: string;
}
export const useOFsAtivas = () => {
const { data: ofsAtivas = [], isLoading } = useQuery({
queryKey: ['ofs-ativas'],
queryFn: async () => {
const { data, error } = await supabase
.from('ficha_tecnica_contratos')
.select('of_number, descricao_resumida, cliente, gestor')
.not('of_number', 'is', null)
.order('of_number');
if (error) {
console.error('Erro ao buscar OFs ativas:', error);
throw error;
}
return data as OFAtiva[];
},
});
return {
ofsAtivas,
data: ofsAtivas, // Mantém compatibilidade com código existente
isLoading,
};
};
+75
View File
@@ -0,0 +1,75 @@
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
export interface OFConcluida {
id: string;
num_of: string;
descritivo?: string;
peso_total?: number;
data_abertura?: string;
data_prazo?: string;
criterio_qualidade?: string;
tratamento_final?: string;
local_uf?: string;
data_arquivamento?: string;
status_detalhado: 'concluida' | 'entregue';
ficha_tecnica_data?: any;
}
export const useOFsConcluidas = () => {
const query = useQuery({
queryKey: ['ofs-concluidas'],
queryFn: async () => {
const { data, error } = await supabase
.from('ofs_concluidas')
.select('*')
.order('data_arquivamento', { ascending: false });
if (error) throw error;
return data as OFConcluida[];
},
});
const entregarOF = async (ofId: string) => {
const { error } = await supabase.rpc('entregar_of', {
of_concluida_id: ofId
});
if (error) throw error;
query.refetch();
};
const reverterEntregueParaConcluida = async (ofId: string) => {
const { error } = await supabase.rpc('reverter_of_entregue_para_concluida', {
of_concluida_id: ofId
});
if (error) throw error;
query.refetch();
};
const reverterConcluidaParaAtiva = async (ofId: string) => {
const { error } = await supabase.rpc('reverter_of_concluida_para_ativa', {
of_concluida_id: ofId
});
if (error) throw error;
query.refetch();
};
// Separar OFs por status
const ofsConcluidas = query.data?.filter(of => of.status_detalhado === 'concluida') || [];
const ofsEntregues = query.data?.filter(of => of.status_detalhado === 'entregue') || [];
return {
...query,
ofsConcluidas,
ofsEntregues,
entregarOF,
reverterEntregueParaConcluida,
reverterConcluidaParaAtiva,
refetch: query.refetch,
loading: query.isLoading
};
};
+404
View File
@@ -0,0 +1,404 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
// Types
export interface ContratoObra {
id: string;
of_number: string;
nome_obra: string | null;
cliente: string | null;
data_inicio_contratual: string | null;
data_termino_prevista: string | null;
status: 'Aguardando Inicio' | 'Ativo' | 'Pausado' | 'Concluído' | 'Arquivada';
created_at: string;
updated_at: string;
}
export interface RecursoObra {
id: string;
tipo_recurso: string;
nome_recurso: string;
descricao: string | null;
ativo: boolean;
created_at: string;
updated_at: string;
}
export interface CondicaoClimatica {
id: string;
nome: string;
icone: string | null;
ativo: boolean;
}
export interface MotivoImprodutivo {
id: string;
motivo: string;
descricao: string | null;
categoria: 'Cliente' | 'Empresa Montadora' | 'Contratada' | 'Terceiros Indiretos';
ativo: boolean;
}
export interface DiarioObraRDO {
id: string;
of_number: string;
usuario_rdo: string | null;
usuario_nome: string | null;
numero_rdo: string | null;
data: string;
condicao_climatica_id: string | null;
temperatura_aproximada: number | null;
hora_inicio: string | null;
hora_fim: string | null;
total_horas_trabalhadas: number | null;
observacoes_gerais: string | null;
finalizado: boolean;
sincronizado: boolean;
created_at: string;
updated_at: string;
condicoes_climaticas?: CondicaoClimatica;
}
// Hooks para Contratos de Obra
export const useContratosObra = () => {
return useQuery({
queryKey: ['contratos_obra'],
queryFn: async () => {
const { data, error } = await supabase
.from('contratos_obra')
.select('*')
.order('created_at', { ascending: false });
if (error) throw error;
return data as ContratoObra[];
},
});
};
export const useCreateContratoObra = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (contrato: Omit<ContratoObra, 'id' | 'created_at' | 'updated_at'>) => {
const { data, error } = await supabase
.from('contratos_obra')
.insert([contrato])
.select()
.single();
if (error) throw error;
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['contratos_obra'] });
toast.success('Contrato de obra criado com sucesso!');
},
onError: (error) => {
toast.error('Erro ao criar contrato de obra: ' + error.message);
},
});
};
export const useUpdateContratoObra = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, ...updates }: Partial<ContratoObra> & { id: string }) => {
const { data, error } = await supabase
.from('contratos_obra')
.update(updates)
.eq('id', id)
.select()
.single();
if (error) throw error;
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['contratos_obra'] });
toast.success('Status da obra atualizado com sucesso!');
},
onError: (error) => {
toast.error('Erro ao atualizar obra: ' + error.message);
},
});
};
// Hooks para Recursos de Obra
export const useRecursosObra = () => {
return useQuery({
queryKey: ['recursos_obra'],
queryFn: async () => {
const { data, error } = await supabase
.from('recursos_obra')
.select('*')
.eq('ativo', true)
.order('tipo_recurso', { ascending: true });
if (error) throw error;
return data as RecursoObra[];
},
});
};
export const useCreateRecursoObra = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (recurso: Omit<RecursoObra, 'id' | 'created_at' | 'updated_at' | 'ativo'>) => {
const { data, error } = await supabase
.from('recursos_obra')
.insert([{ ...recurso, ativo: true }])
.select()
.single();
if (error) throw error;
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['recursos_obra'] });
toast.success('Recurso de obra criado com sucesso!');
},
onError: (error) => {
toast.error('Erro ao criar recurso de obra: ' + error.message);
},
});
};
// Hooks para Condições Climáticas
export const useCondicoesClimaticas = () => {
return useQuery({
queryKey: ['condicoes_climaticas'],
queryFn: async () => {
const { data, error } = await supabase
.from('condicoes_climaticas')
.select('*')
.eq('ativo', true)
.order('nome', { ascending: true });
if (error) throw error;
return data as CondicaoClimatica[];
},
});
};
// Hooks para Motivos Improdutivos
export const useMotivosImprodutivos = () => {
return useQuery({
queryKey: ['motivos_improdutivos'],
queryFn: async () => {
const { data, error } = await supabase
.from('motivos_improdutivos')
.select('*')
.eq('ativo', true)
.order('categoria', { ascending: true });
if (error) throw error;
return data as MotivoImprodutivo[];
},
});
};
export const useCreateMotivoImprodutivo = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (motivo: Omit<MotivoImprodutivo, 'id' | 'ativo'>) => {
const { data, error } = await supabase
.from('motivos_improdutivos')
.insert([{ ...motivo, ativo: true }])
.select()
.single();
if (error) throw error;
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['motivos_improdutivos'] });
toast.success('Motivo improdutivo criado com sucesso!');
},
onError: (error) => {
toast.error('Erro ao criar motivo improdutivo: ' + error.message);
},
});
};
export const useUpdateMotivoImprodutivo = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, ...updates }: Partial<MotivoImprodutivo> & { id: string }) => {
const { data, error } = await supabase
.from('motivos_improdutivos')
.update(updates)
.eq('id', id)
.select()
.single();
if (error) throw error;
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['motivos_improdutivos'] });
toast.success('Motivo improdutivo atualizado com sucesso!');
},
onError: (error) => {
toast.error('Erro ao atualizar motivo improdutivo: ' + error.message);
},
});
};
export const useDeleteMotivoImprodutivo = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (id: string) => {
const { error } = await supabase
.from('motivos_improdutivos')
.update({ ativo: false })
.eq('id', id);
if (error) throw error;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['motivos_improdutivos'] });
toast.success('Motivo improdutivo removido com sucesso!');
},
onError: (error) => {
toast.error('Erro ao remover motivo improdutivo: ' + error.message);
},
});
};
// Hooks para Diário de Obra (RDO)
export const useDiariosObra = (ofNumber?: string) => {
return useQuery({
queryKey: ['diarios_obra', ofNumber],
queryFn: async () => {
let query = supabase
.from('diario_obra_rdo')
.select(`
*,
condicoes_climaticas:condicao_climatica_id(*)
`)
.order('data', { ascending: false });
if (ofNumber) {
query = query.eq('of_number', ofNumber);
}
const { data, error } = await query;
if (error) throw error;
return data as DiarioObraRDO[];
},
enabled: !!ofNumber,
});
};
// Hook para verificar se existe RDO para OF e data
export const useCheckExistingRDO = () => {
return useMutation({
mutationFn: async ({ ofNumber, data }: { ofNumber: string; data: string }) => {
const { data: existingRDO, error } = await supabase
.from('diario_obra_rdo')
.select(`
*,
condicoes_climaticas:condicao_climatica_id(*)
`)
.eq('of_number', ofNumber)
.eq('data', data)
.maybeSingle();
if (error) throw error;
return existingRDO;
},
});
};
export const useCreateDiarioObra = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (rdo: Omit<DiarioObraRDO, 'id' | 'created_at' | 'updated_at' | 'condicoes_climaticas' | 'numero_rdo'>) => {
// Buscar nome do usuário atual
const { data: { user } } = await supabase.auth.getUser();
let userName = null;
if (user) {
const { data: profile } = await supabase
.from('profiles')
.select('full_name')
.eq('id', user.id)
.single();
userName = profile?.full_name || user.email?.split('@')[0] || 'Usuário';
}
const { data, error } = await supabase
.from('diario_obra_rdo')
.insert([{ ...rdo, usuario_nome: userName }])
.select(`
*,
condicoes_climaticas:condicao_climatica_id(*)
`)
.single();
if (error) throw error;
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['diarios_obra'] });
toast.success('RDO criado com sucesso!');
},
onError: (error) => {
toast.error('Erro ao criar RDO: ' + error.message);
},
});
};
export const useUpdateDiarioObra = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, ...updates }: Partial<DiarioObraRDO> & { id: string }) => {
const { data, error } = await supabase
.from('diario_obra_rdo')
.update(updates)
.eq('id', id)
.select(`
*,
condicoes_climaticas:condicao_climatica_id(*)
`)
.single();
if (error) throw error;
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['diarios_obra'] });
toast.success('RDO atualizado com sucesso!');
},
onError: (error) => {
toast.error('Erro ao atualizar RDO: ' + error.message);
},
});
};
// Hook para buscar OFs disponíveis
export const useOFsDisponiveis = () => {
return useQuery({
queryKey: ['ofs_disponiveis'],
queryFn: async () => {
const { data, error } = await supabase
.from('ordens_fabricacao')
.select('num_of, descritivo, data_prazo')
.eq('status', 'ativa')
.order('num_of', { ascending: true });
if (error) throw error;
return data;
},
});
};
+49
View File
@@ -0,0 +1,49 @@
import { useState } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
export function usePasswordManagement() {
const [isChangingPassword, setIsChangingPassword] = useState(false);
const changeUserPassword = async (userId: string, newPassword: string): Promise<boolean> => {
if (!newPassword || newPassword.trim().length === 0) {
toast.error('A senha não pode estar vazia');
return false;
}
if (newPassword.length < 3) {
toast.error('A senha deve ter pelo menos 3 caracteres');
return false;
}
setIsChangingPassword(true);
try {
const { data, error } = await supabase.rpc('admin_change_user_password', {
user_id_param: userId,
new_password: newPassword
});
if (error) {
console.error('Error changing password:', error);
toast.error('Erro ao alterar senha: ' + error.message);
return false;
}
toast.success('Senha alterada com sucesso');
return true;
} catch (error) {
console.error('Error changing password:', error);
toast.error('Erro ao alterar senha');
return false;
} finally {
setIsChangingPassword(false);
}
};
return {
changeUserPassword,
isChangingPassword
};
}
+95
View File
@@ -0,0 +1,95 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
export const usePasswordReset = () => {
const queryClient = useQueryClient();
// Mutation para solicitar redefinição de senha
const requestPasswordReset = useMutation({
mutationFn: async ({ email }: { email: string }) => {
console.log('🔄 Solicitando redefinição de senha para:', email);
// Detectar o domínio atual e configurar URL de redirecionamento
const currentHostname = window.location.hostname;
let redirectUrl = '';
if (currentHostname.includes('preview--tracksteel.lovable.app') ||
currentHostname.includes('lovableproject.com') ||
currentHostname.includes('lovable.app')) {
// Ambiente de preview do Lovable
redirectUrl = `${window.location.origin}/auth?type=recovery`;
} else if (currentHostname.includes('tracksteel.com.br')) {
// Domínio customizado
redirectUrl = `https://app.tracksteel.com.br/auth?type=recovery`;
} else {
// Fallback para localhost ou outros casos
redirectUrl = `${window.location.origin}/auth?type=recovery`;
}
console.log('📍 URL de redirecionamento configurada:', redirectUrl);
// Enviar e-mail de redefinição usando o Supabase Auth
const { error: resetError } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: redirectUrl,
});
if (resetError) {
console.error('❌ Erro ao solicitar redefinição:', resetError);
throw resetError;
}
console.log('✅ E-mail de redefinição enviado com sucesso para:', email);
// Registrar a solicitação no banco de dados (opcional, não bloquear se falhar)
try {
const { error: insertError } = await supabase
.from('password_reset_requests')
.insert({
email,
user_id: null,
ip_address: null,
user_agent: navigator.userAgent,
});
if (insertError) {
console.warn('⚠️ Falha ao registrar solicitação:', insertError);
} else {
console.log('📝 Solicitação registrada no banco de dados');
}
} catch (logError) {
console.warn('⚠️ Erro no log da solicitação:', logError);
}
return { success: true };
},
onSuccess: () => {
toast.success('E-mail de redefinição enviado com sucesso! Verifique sua caixa de entrada e clique no link recebido.');
queryClient.invalidateQueries({ queryKey: ['password-reset-requests'] });
},
onError: (error: any) => {
console.error('❌ Erro na solicitação de redefinição:', error);
// Tratamento específico de erros
let errorMessage = 'Erro ao solicitar redefinição de senha. Tente novamente.';
if (error.message?.includes('User not found')) {
errorMessage = 'E-mail não encontrado no sistema. Verifique se o e-mail está correto.';
} else if (error.message?.includes('rate limit') || error.message?.includes('Email rate limit exceeded')) {
errorMessage = 'Muitas tentativas de redefinição. Aguarde alguns minutos antes de tentar novamente.';
} else if (error.message?.includes('signup_disabled')) {
errorMessage = 'Funcionalidade temporariamente indisponível. Contate o administrador.';
} else if (error.message) {
errorMessage = `Erro: ${error.message}`;
}
toast.error(errorMessage);
},
});
return {
requestPasswordReset: requestPasswordReset.mutate,
isRequesting: requestPasswordReset.isPending,
};
};
+647
View File
@@ -0,0 +1,647 @@
import { useState, useEffect, useCallback } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
import { useAuth } from '@/hooks/useAuth';
export interface Peca {
id: string;
created_at: string;
of_number: string;
etapa_fase: string;
marca: string;
descricao: string;
quantidade: number;
peso_unitario: number;
peso_total: number;
tratamento_superficial: string;
material: string;
perfil_principal: string;
tem_componentes: boolean;
user_id: string;
prioridade: string;
quantidadeDisponivel?: number;
}
interface PecaPrioridade {
peca_id: string;
prioridade_mais_alta: string;
}
export function usePecas() {
const { user } = useAuth();
const [pecas, setPecas] = useState<Peca[]>([]);
const [loading, setLoading] = useState(true);
const [ofNumbers, setOfNumbers] = useState<string[]>([]);
const [hasRecentImport, setHasRecentImport] = useState(false);
const loadPecas = useCallback(async () => {
console.log('🔄 Carregando peças...');
try {
const { data, error } = await supabase
.from('pecas')
.select('*')
.order('created_at', { ascending: false });
if (error) {
console.error('❌ Erro ao carregar peças:', error);
throw error;
}
console.log('✅ Peças carregadas:', data?.length || 0);
setPecas(data || []);
const uniqueOFs = Array.from(new Set(data?.map(peca => peca.of_number).filter(Boolean) || []));
setOfNumbers(uniqueOFs);
return data || [];
} catch (error) {
console.error('❌ Erro ao carregar peças:', error);
toast.error('Erro ao carregar peças');
return [];
}
}, []);
const sincronizarPrioridades = useCallback(async (showToast = true) => {
try {
console.log('🔄 Sincronizando prioridades...');
const { data: pecasComPrioridades, error } = await supabase
.rpc('calcular_prioridades_pecas_bulk') as { data: PecaPrioridade[] | null, error: any };
if (error) {
console.error('❌ Erro ao calcular prioridades:', error);
return;
}
console.log('✅ Prioridades calculadas:', pecasComPrioridades?.length || 0);
if (pecasComPrioridades && Array.isArray(pecasComPrioridades) && pecasComPrioridades.length > 0) {
const updates = pecasComPrioridades.map(p => ({
id: p.peca_id,
prioridade: p.prioridade_mais_alta
}));
const batchSize = 100;
for (let i = 0; i < updates.length; i += batchSize) {
const batch = updates.slice(i, i + batchSize);
for (const update of batch) {
const { error: updateError } = await supabase
.from('pecas')
.update({ prioridade: update.prioridade })
.eq('id', update.id);
if (updateError) {
console.error('❌ Erro ao atualizar prioridade da peça:', updateError);
}
}
}
console.log('✅ Prioridades sincronizadas com sucesso');
return await loadPecas();
}
} catch (error) {
console.error('❌ Erro ao sincronizar prioridades:', error);
if (showToast) {
toast.error('Erro ao sincronizar prioridades');
}
}
}, [loadPecas]);
// Carregamento inicial otimizado - sem sincronização automática
useEffect(() => {
const initializeData = async () => {
try {
setLoading(true);
console.log('🚀 Iniciando carregamento da tela Cadastro de Peças...');
// Apenas carrega as peças inicialmente
await loadPecas();
console.log('✅ Carregamento inicial concluído');
} catch (error) {
console.error('❌ Erro no carregamento inicial:', error);
} finally {
setLoading(false);
}
};
initializeData();
}, [loadPecas]);
// Listeners otimizados - menos triggers automáticos
useEffect(() => {
const channel = supabase
.channel('pecas_changes')
.on('postgres_changes', { event: '*', schema: 'public', table: 'pecas' }, (payload) => {
console.log('📡 Mudança detectada em pecas:', payload);
// Apenas recarrega se não for atualização de prioridade
if (payload.eventType !== 'UPDATE' || !payload.new?.prioridade) {
if (!loading) {
loadPecas();
}
}
})
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [loading, loadPecas]);
const checkRecentImport = useCallback(async () => {
try {
const { data, error, count } = await supabase
.from('pecas')
.select('*', { count: 'exact', head: true })
.eq('user_id', user?.id)
.gte('created_at', new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString());
if (error) {
console.error("Erro ao verificar importação recente:", error);
return;
}
setHasRecentImport(count !== null && count > 0);
console.log(`Importação recente detectada: ${count !== null && count > 0}`);
} catch (error) {
console.error("Erro ao verificar importação recente:", error);
}
}, [user?.id]);
const savePeca = async (pecaData: any): Promise<boolean> => {
if (!user) {
toast.error('Usuário não autenticado');
return false;
}
try {
const { prioridade, ...cleanedData } = pecaData;
const { data, error } = await supabase
.from('pecas')
.insert([{ ...cleanedData, user_id: user.id, prioridade: 'P4' }])
.select();
if (error) {
console.error('Erro ao salvar peça:', error);
toast.error('Erro ao salvar peça');
return false;
}
toast.success('Peça salva com sucesso!');
// Sincronização manual apenas quando necessário
setTimeout(() => {
sincronizarPrioridades(false);
}, 2000);
await loadPecas();
return true;
} catch (error) {
console.error('Erro ao salvar peça:', error);
toast.error('Erro ao salvar peça');
return false;
}
};
const updatePeca = async (id: string, pecaData: any): Promise<boolean> => {
if (!user) {
toast.error('Usuário não autenticado');
return false;
}
try {
const { prioridade, ...cleanedData } = pecaData;
const { data, error } = await supabase
.from('pecas')
.update(cleanedData)
.eq('id', id)
.select();
if (error) {
console.error('Erro ao atualizar peça:', error);
toast.error('Erro ao atualizar peça');
return false;
}
toast.success('Peça atualizada com sucesso!');
// Sincronização manual apenas quando necessário
setTimeout(() => {
sincronizarPrioridades(false);
}, 2000);
await loadPecas();
return true;
} catch (error) {
console.error('Erro ao atualizar peça:', error);
toast.error('Erro ao atualizar peça');
return false;
}
};
const deletePeca = async (pecaId: string): Promise<boolean> => {
if (!user) {
toast.error('Usuário não autenticado');
return false;
}
try {
const { error } = await supabase
.from('pecas')
.delete()
.eq('id', pecaId);
if (error) {
console.error('Erro ao apagar peça:', error);
toast.error('Erro ao apagar peça');
return false;
}
toast.success('Peça apagada com sucesso!');
await loadPecas();
return true;
} catch (error) {
console.error('Erro ao apagar peça:', error);
toast.error('Erro ao apagar peça');
return false;
}
};
const importCSV = async (file: File) => {
console.log('📄 Importando CSV...');
if (!user) {
toast.error('Usuário não autenticado');
return;
}
try {
const text = await file.text();
const lines = text.split('\n').filter(line => line.trim());
if (lines.length < 2) {
toast.error('Arquivo CSV vazio ou inválido');
return;
}
const headers = lines[0].split(',').map(h => h.trim());
const pecasData = [];
for (let i = 1; i < lines.length; i++) {
const values = lines[i].split(',').map(v => v.trim());
if (values.length === headers.length) {
const peca: any = { user_id: user.id, prioridade: 'P4' };
headers.forEach((header, index) => {
const value = values[index];
if (header === 'quantidade' || header === 'peso_unitario' || header === 'peso_total') {
peca[header] = parseFloat(value) || 0;
} else if (header === 'tem_componentes') {
peca[header] = ['true', '1', 'sim'].includes(value.toLowerCase());
} else if (header !== 'prioridade') {
peca[header] = value;
}
});
pecasData.push(peca);
}
}
if (pecasData.length === 0) {
toast.error('Nenhuma peça válida encontrada no arquivo CSV');
return;
}
const { error } = await supabase
.from('pecas')
.insert(pecasData);
if (error) {
console.error('❌ Erro ao importar CSV:', error);
throw error;
}
console.log('✅ CSV importado com sucesso:', pecasData.length, 'peças');
toast.success(`${pecasData.length} peça(s) importada(s) com sucesso!`);
setHasRecentImport(true);
// Sincronização manual apenas após importação
setTimeout(() => {
sincronizarPrioridades(false);
}, 3000);
await loadPecas();
} catch (error) {
console.error('❌ Erro ao importar CSV:', error);
toast.error('Erro ao importar arquivo CSV');
throw error;
}
};
const importPecas = async (pecasData: any[]) => {
console.log('📦 Iniciando importação de peças:', pecasData.length);
if (!user) {
toast.error('Usuário não autenticado');
return;
}
try {
const pecasMap = new Map<string, any>();
const componentesMap = new Map<string, any[]>();
console.log('🔍 Separando dados de peças e componentes...');
pecasData.forEach((item, index) => {
console.log(`📋 Processando item ${index + 1}:`, item);
const pecaKey = `${item.of_number || ''}-${item.etapa_fase || ''}-${item.marca || ''}`;
const pecaData = {
of_number: item.of_number || '',
etapa_fase: item.etapa_fase || '',
marca: item.marca || '',
descricao: item.descricao || '',
quantidade: Number(item.quantidade) || 0,
peso_unitario: Math.round(Number(item.peso_unitario) || 0),
peso_total: Math.round(Number(item.peso_total) || 0),
tratamento_superficial: item.tratamento_superficial || '',
material: item.material || '',
perfil_principal: item.perfil_principal || '',
tem_componentes: Boolean(item.tem_componentes),
user_id: user.id,
prioridade: 'P4'
};
if (!pecasMap.has(pecaKey)) {
pecasMap.set(pecaKey, pecaData);
}
if (item.marca_componente && item.marca_componente.trim()) {
const componenteData = {
marca_componente: item.marca_componente.trim(),
descricao: item.descricao_componente || '',
perfil: item.perfil_componente || '',
peso_unitario: Number(item.peso_unitario_componente) || 0,
quantidade_por_peca: Number(item.quantidade_por_peca) || 1,
user_id: user.id
};
if (!componentesMap.has(pecaKey)) {
componentesMap.set(pecaKey, []);
}
componentesMap.get(pecaKey)!.push(componenteData);
}
});
const pecasParaInserir = Array.from(pecasMap.values());
console.log('✅ Peças para inserir:', pecasParaInserir.length);
console.log('✅ Peças com componentes:', componentesMap.size);
if (pecasParaInserir.length === 0) {
toast.error('Nenhuma peça válida encontrada para importação');
return;
}
console.log('💾 Inserindo peças principais...');
const { data: pecasInseridas, error: pecasError } = await supabase
.from('pecas')
.insert(pecasParaInserir)
.select('id, of_number, etapa_fase, marca');
if (pecasError) {
console.error('❌ Erro ao inserir peças:', pecasError);
toast.error(`Erro ao inserir peças: ${pecasError.message}`);
throw pecasError;
}
console.log('✅ Peças inseridas com sucesso:', pecasInseridas?.length || 0);
let totalComponentesInseridos = 0;
if (componentesMap.size > 0 && pecasInseridas) {
console.log('🔧 Inserindo componentes...');
for (const [pecaKey, componentes] of componentesMap.entries()) {
const [of_number, etapa_fase, marca] = pecaKey.split('-');
const pecaInserida = pecasInseridas.find(p =>
p.of_number === of_number &&
p.etapa_fase === etapa_fase &&
p.marca === marca
);
if (pecaInserida && componentes.length > 0) {
const componentesComPecaId = componentes.map(comp => ({
...comp,
peca_id: pecaInserida.id
}));
console.log(`🔧 Inserindo ${componentesComPecaId.length} componentes para peça ${marca}...`);
const { error: componentesError } = await supabase
.from('componentes_peca')
.insert(componentesComPecaId);
if (componentesError) {
console.error('❌ Erro ao inserir componentes:', componentesError);
} else {
totalComponentesInseridos += componentesComPecaId.length;
console.log(`✅ Componentes inseridos para peça ${marca}: ${componentesComPecaId.length}`);
}
}
}
}
let successMessage = `${pecasInseridas?.length || 0} peça(s) importada(s) com sucesso!`;
if (totalComponentesInseridos > 0) {
successMessage += ` ${totalComponentesInseridos} componente(s) também foram criados.`;
}
console.log('🎉 Importação concluída:', successMessage);
toast.success(successMessage);
setHasRecentImport(true);
// Sincronização manual apenas após importação
setTimeout(() => {
sincronizarPrioridades(false);
}, 3000);
await loadPecas();
} catch (error) {
console.error('❌ Erro ao importar peças:', error);
toast.error(`Erro ao importar peças: ${error instanceof Error ? error.message : 'Erro desconhecido'}`);
throw error;
}
};
const undoLastImport = async () => {
if (!user) {
toast.error('Usuário não autenticado');
return;
}
try {
const { data: pecasParaApagar, error: selectError } = await supabase
.from('pecas')
.select('id')
.eq('user_id', user.id)
.gte('created_at', new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString());
if (selectError) {
console.error('Erro ao selecionar peças para apagar:', selectError);
toast.error('Erro ao selecionar peças para apagar');
return;
}
if (!pecasParaApagar || pecasParaApagar.length === 0) {
toast.info('Nenhuma peça recente para desfazer a importação.');
return;
}
const pecaIds = pecasParaApagar.map(peca => peca.id);
const { error: deleteComponentesError } = await supabase
.from('componentes_peca')
.delete()
.in('peca_id', pecaIds);
if (deleteComponentesError) {
console.error('Erro ao apagar componentes das peças:', deleteComponentesError);
toast.error('Erro ao apagar componentes das peças');
return;
}
const { error: deleteError } = await supabase
.from('pecas')
.delete()
.in('id', pecaIds);
if (deleteError) {
console.error('Erro ao apagar peças:', deleteError);
toast.error('Erro ao apagar peças');
return;
}
toast.success('Última importação desfeita com sucesso!');
setHasRecentImport(false);
await loadPecas();
} catch (error) {
console.error('Erro ao desfazer importação:', error);
toast.error('Erro ao desfazer importação');
}
};
const deleteLastImport = async () => {
if (!user) {
toast.error('Usuário não autenticado');
return;
}
try {
const { data: pecasParaApagar, error: selectError } = await supabase
.from('pecas')
.select('id')
.eq('user_id', user.id)
.gte('created_at', new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString());
if (selectError) {
console.error('Erro ao selecionar peças para apagar:', selectError);
toast.error('Erro ao selecionar peças para apagar');
return;
}
if (!pecasParaApagar || pecasParaApagar.length === 0) {
toast.info('Nenhuma peça recente para apagar.');
return;
}
const pecaIds = pecasParaApagar.map(peca => peca.id);
const { error: deleteComponentesError } = await supabase
.from('componentes_peca')
.delete()
.in('peca_id', pecaIds);
if (deleteComponentesError) {
console.error('Erro ao apagar componentes das peças:', deleteComponentesError);
toast.error('Erro ao apagar componentes das peças');
return;
}
const { error: deleteError } = await supabase
.from('pecas')
.delete()
.in('id', pecaIds);
if (deleteError) {
console.error('Erro ao apagar peças:', deleteError);
toast.error('Erro ao apagar peças');
return;
}
toast.success('Última importação apagada com sucesso!');
setHasRecentImport(false);
await loadPecas();
} catch (error) {
console.error('Erro ao apagar importação:', error);
toast.error('Erro ao apagar importação');
}
};
const batchUpdatePecas = async (pecaIds: string[], updates: any) => {
if (!user) {
toast.error('Usuário não autenticado');
return;
}
try {
const { prioridade, ...cleanedUpdates } = updates;
const { error } = await supabase
.from('pecas')
.update(cleanedUpdates)
.in('id', pecaIds);
if (error) {
console.error('Erro ao atualizar peças em lote:', error);
toast.error('Erro ao atualizar peças em lote');
throw error;
}
toast.success(`${pecaIds.length} peças atualizadas com sucesso!`);
// Sincronização manual apenas quando necessário
setTimeout(() => {
sincronizarPrioridades(false);
}, 2000);
await loadPecas();
} catch (error) {
console.error('Erro ao atualizar peças em lote:', error);
toast.error('Erro ao atualizar peças em lote');
throw error;
}
};
useEffect(() => {
if (user?.id) {
checkRecentImport();
}
}, [user?.id, checkRecentImport]);
return {
pecas,
loading,
ofNumbers,
savePeca,
updatePeca,
deletePeca,
importCSV,
importPecas,
undoLastImport,
deleteLastImport,
hasRecentImport,
batchUpdatePecas,
loadPecas,
sincronizarPrioridades
};
}
+529
View File
@@ -0,0 +1,529 @@
import { useState } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
import { useAuth } from '@/hooks/useAuth';
interface CSVRow {
// Campos da peça principal
of_number: string;
etapa_fase: string;
marca: string;
descricao: string;
quantidade: string;
peso_unitario: string;
peso_total: string;
tratamento_superficial: string;
material: string;
perfil_principal: string;
tem_componentes: string;
// Campos do componente
marca_componente: string;
descricao_componente: string;
perfil_componente: string;
peso_unitario_componente: string;
quantidade_por_peca: string;
}
interface ProcessedPeca {
of_number: string;
etapa_fase: string;
marca: string;
descricao: string;
quantidade: number;
peso_unitario: number;
peso_total: number;
tratamento_superficial: string;
material: string;
perfil_principal: string;
tem_componentes: boolean;
user_id: string;
componentes: {
marca_componente: string;
descricao: string;
perfil: string;
peso_unitario: number;
quantidade_por_peca: number;
}[];
}
export function usePecasCSVImport() {
const { user } = useAuth();
const [importLoading, setImportLoading] = useState(false);
const exportCompleteTemplateCSV = () => {
const headers = [
'of_number',
'etapa_fase',
'marca',
'descricao',
'quantidade',
'peso_unitario',
'peso_total',
'tratamento_superficial',
'material',
'perfil_principal',
'tem_componentes',
'marca_componente',
'descricao_componente',
'perfil_componente',
'peso_unitario_componente',
'quantidade_por_peca'
];
// Exemplo com dados para orientação
const exampleData = [
'12345,Montagem,P001,Viga Principal,5,150.5,752.5,Galvanizado,Aço,IPE200,true,C001,Parafuso M16,M16x50,0.2,8',
'12345,Montagem,P001,Viga Principal,5,150.5,752.5,Galvanizado,Aço,IPE200,true,C002,Porca M16,M16,0.1,8',
'12346,Soldagem,P002,Coluna,3,200.0,600.0,Pintado,Aço,HEA300,false,,,,0,0'
];
const csvContent = [
headers.join(','),
...exampleData
].join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'modelo_pecas_completo.csv';
link.click();
toast.success('Modelo CSV completo baixado com sucesso!');
};
const exportExampleDataCSV = async () => {
try {
// Buscar peças do banco de dados
const { data: pecas, error: pecasError } = await supabase
.from('pecas')
.select('*')
.limit(5);
if (pecasError) throw pecasError;
if (!pecas || pecas.length === 0) {
toast.error('Nenhuma peça encontrada no banco de dados');
return;
}
const headers = [
'of_number',
'etapa_fase',
'marca',
'descricao',
'quantidade',
'peso_unitario',
'peso_total',
'tratamento_superficial',
'material',
'perfil_principal',
'tem_componentes',
'marca_componente',
'descricao_componente',
'perfil_componente',
'peso_unitario_componente',
'quantidade_por_peca'
];
const csvRows: string[] = [headers.join(',')];
for (const peca of pecas) {
// Buscar componentes da peça
const { data: componentes } = await supabase
.from('componentes_peca')
.select('*')
.eq('peca_id', peca.id);
if (componentes && componentes.length > 0) {
// Para cada componente, criar uma linha
componentes.forEach(comp => {
const row = [
peca.of_number || '',
peca.etapa_fase || '',
peca.marca || '',
peca.descricao || '',
peca.quantidade || 0,
peca.peso_unitario || 0,
peca.peso_total || 0,
peca.tratamento_superficial || '',
peca.material || '',
peca.perfil_principal || '',
peca.tem_componentes ? 'true' : 'false',
comp.marca_componente || '',
comp.descricao || '',
comp.perfil || '',
comp.peso_unitario || 0,
comp.quantidade_por_peca || 1
];
csvRows.push(row.map(field => `"${field}"`).join(','));
});
} else {
// Peça sem componentes
const row = [
peca.of_number || '',
peca.etapa_fase || '',
peca.marca || '',
peca.descricao || '',
peca.quantidade || 0,
peca.peso_unitario || 0,
peca.peso_total || 0,
peca.tratamento_superficial || '',
peca.material || '',
peca.perfil_principal || '',
peca.tem_componentes ? 'true' : 'false',
'',
'',
'',
0,
0
];
csvRows.push(row.map(field => `"${field}"`).join(','));
}
}
const csvContent = csvRows.join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `exemplo_dados_reais_${new Date().toISOString().split('T')[0]}.csv`;
link.click();
toast.success('Arquivo de exemplo com dados reais exportado com sucesso!');
} catch (error) {
console.error('Erro ao exportar dados de exemplo:', error);
toast.error('Erro ao gerar arquivo de exemplo');
}
};
const checkExistingPieces = async (pecas: ProcessedPeca[]) => {
console.log('🔍 Verificando peças existentes:', pecas);
const duplicates: string[] = [];
const existingPiecesForComponents: { [key: string]: string } = {};
for (const peca of pecas) {
console.log(`🔍 Verificando peça: OF=${peca.of_number}, Fase=${peca.etapa_fase}, Marca=${peca.marca}`);
const { data: existingPecas, error } = await supabase
.from('pecas')
.select('id, marca, of_number, etapa_fase')
.eq('of_number', peca.of_number)
.eq('etapa_fase', peca.etapa_fase)
.eq('marca', peca.marca);
if (error) {
console.error('❌ Erro ao verificar peça existente:', error);
continue;
}
console.log('📋 Peças encontradas no banco:', existingPecas);
if (existingPecas && existingPecas.length > 0) {
const existingPeca = existingPecas[0];
console.log(`⚠️ Peça já existe: ${existingPeca.marca} (${existingPeca.of_number}/${existingPeca.etapa_fase})`);
if (peca.componentes.length > 0) {
let hasNewComponents = false;
for (const componente of peca.componentes) {
const { data: existingComponent } = await supabase
.from('componentes_peca')
.select('id, marca_componente')
.eq('peca_id', existingPeca.id)
.eq('marca_componente', componente.marca_componente.trim());
if (!existingComponent || existingComponent.length === 0) {
console.log(`🆕 Componente ${componente.marca_componente} é NOVO para a peça ${existingPeca.marca}`);
hasNewComponents = true;
}
}
if (hasNewComponents) {
existingPiecesForComponents[`${peca.of_number}-${peca.etapa_fase}-${peca.marca}`] = existingPeca.id;
} else {
duplicates.push(`Peça ${peca.marca} (OF: ${peca.of_number}, Fase: ${peca.etapa_fase}) já está cadastrada`);
}
} else {
duplicates.push(`Peça ${peca.marca} (OF: ${peca.of_number}, Fase: ${peca.etapa_fase}) já está cadastrada`);
}
}
}
console.log('❌ Duplicatas encontradas:', duplicates);
console.log('🔄 Peças existentes para adicionar componentes:', existingPiecesForComponents);
return { duplicates, existingPiecesForComponents };
};
const importCompleteCSV = async (file: File, onSuccess: () => void) => {
console.log('🚀 Iniciando importação CSV:', file.name);
if (!file || !file.name.endsWith('.csv')) {
toast.error('Selecione um arquivo CSV válido');
return;
}
if (!user) {
toast.error('Usuário não autenticado');
return;
}
setImportLoading(true);
try {
const text = await file.text();
const lines = text.split('\n').filter(line => line.trim());
console.log('📄 Total de linhas no arquivo:', lines.length);
if (lines.length < 2) {
toast.error('Arquivo CSV deve conter pelo menos uma linha de dados');
return;
}
const headers = lines[0].split(',').map(h => h.trim().replace(/"/g, ''));
const expectedHeaders = [
'of_number', 'etapa_fase', 'marca', 'descricao', 'quantidade',
'peso_unitario', 'peso_total', 'tratamento_superficial', 'material',
'perfil_principal', 'tem_componentes', 'marca_componente',
'descricao_componente', 'perfil_componente', 'peso_unitario_componente',
'quantidade_por_peca'
];
console.log('📋 Headers encontrados:', headers);
console.log('📋 Headers esperados:', expectedHeaders);
const hasRequiredHeaders = expectedHeaders.every(header => headers.includes(header));
if (!hasRequiredHeaders) {
const missingHeaders = expectedHeaders.filter(h => !headers.includes(h));
console.error('❌ Headers faltando:', missingHeaders);
toast.error('Arquivo CSV deve conter todas as colunas necessárias. Faltam: ' + missingHeaders.join(', '));
return;
}
// Processar linhas do CSV
const csvRows: CSVRow[] = [];
for (let i = 1; i < lines.length; i++) {
const values = lines[i].split(',').map(v => v.trim().replace(/"/g, ''));
if (values.length !== headers.length) {
console.warn(`⚠️ Linha ${i + 1} tem ${values.length} valores, esperado ${headers.length}`);
continue;
}
const row: any = {};
headers.forEach((header, index) => {
row[header] = values[index] || '';
});
csvRows.push(row as CSVRow);
}
console.log('📊 Dados do CSV processados:', csvRows.length, 'linhas');
// Agrupar por peça principal
const pecasMap = new Map<string, ProcessedPeca>();
csvRows.forEach(row => {
const pecaKey = `${row.of_number.trim()}-${row.etapa_fase.trim()}-${row.marca.trim()}`;
if (!pecasMap.has(pecaKey)) {
// Criar nova peça
const temComponentes = ['true', '1', 'sim', 'yes'].includes(row.tem_componentes.toLowerCase().trim());
pecasMap.set(pecaKey, {
of_number: row.of_number.trim(),
etapa_fase: row.etapa_fase.trim(),
marca: row.marca.trim(),
descricao: row.descricao.trim(),
quantidade: parseInt(row.quantidade) || 0,
peso_unitario: parseFloat(row.peso_unitario) || 0,
peso_total: parseFloat(row.peso_total) || 0,
tratamento_superficial: row.tratamento_superficial.trim(),
material: row.material.trim(),
perfil_principal: row.perfil_principal.trim(),
tem_componentes: temComponentes,
user_id: user.id,
componentes: []
});
}
// Adicionar componente se existir
if (row.marca_componente && row.marca_componente.trim()) {
const peca = pecasMap.get(pecaKey)!;
peca.componentes.push({
marca_componente: row.marca_componente.trim(),
descricao: row.descricao_componente?.trim() || '',
perfil: row.perfil_componente?.trim() || '',
peso_unitario: parseFloat(row.peso_unitario_componente) || 0,
quantidade_por_peca: parseInt(row.quantidade_por_peca) || 1
});
}
});
const pecas = Array.from(pecasMap.values());
console.log('🔢 Peças processadas:', pecas.length);
if (pecas.length === 0) {
toast.error('Nenhuma peça válida encontrada no arquivo');
return;
}
// Verificar duplicatas
const { duplicates, existingPiecesForComponents } = await checkExistingPieces(pecas);
// Separar peças novas das que só precisam de componentes adicionais
const newPecas = pecas.filter(peca =>
!existingPiecesForComponents[`${peca.of_number}-${peca.etapa_fase}-${peca.marca}`]
);
console.log('✨ Peças novas para inserir:', newPecas.length);
console.log('🔄 Peças existentes para adicionar componentes:', Object.keys(existingPiecesForComponents).length);
let totalPecasInseridas = 0;
let totalComponentesInseridos = 0;
// Inserir peças principais novas
if (newPecas.length > 0) {
console.log('💾 Inserindo peças principais...');
const pecasData = newPecas.map(peca => ({
of_number: peca.of_number,
etapa_fase: peca.etapa_fase,
marca: peca.marca,
descricao: peca.descricao,
quantidade: peca.quantidade,
peso_unitario: peca.peso_unitario,
peso_total: peca.peso_total,
tratamento_superficial: peca.tratamento_superficial,
material: peca.material,
perfil_principal: peca.perfil_principal,
tem_componentes: peca.tem_componentes,
user_id: peca.user_id
}));
const { data: pecasInseridas, error: pecasError } = await supabase
.from('pecas')
.insert(pecasData)
.select('id, marca, of_number, etapa_fase');
if (pecasError) {
console.error('❌ Erro ao inserir peças:', pecasError);
throw new Error(`Erro ao inserir peças: ${pecasError.message}`);
}
totalPecasInseridas = pecasInseridas?.length || 0;
console.log('✅ Peças inseridas:', totalPecasInseridas);
// Inserir componentes das peças novas
for (const peca of newPecas) {
if (peca.componentes.length > 0) {
const pecaInserida = pecasInseridas?.find(p =>
p.marca === peca.marca &&
p.of_number === peca.of_number &&
p.etapa_fase === peca.etapa_fase
);
if (pecaInserida) {
const componentesData = peca.componentes.map(comp => ({
peca_id: pecaInserida.id,
marca_componente: comp.marca_componente,
descricao: comp.descricao,
perfil: comp.perfil,
peso_unitario: comp.peso_unitario,
quantidade_por_peca: comp.quantidade_por_peca,
user_id: user.id
}));
console.log(`🔧 Inserindo ${componentesData.length} componentes para peça ${peca.marca}...`);
const { error: componentesError } = await supabase
.from('componentes_peca')
.insert(componentesData);
if (componentesError) {
console.error('❌ Erro ao inserir componentes:', componentesError);
throw new Error(`Erro ao inserir componentes: ${componentesError.message}`);
}
totalComponentesInseridos += componentesData.length;
}
}
}
}
// Inserir componentes adicionais em peças existentes
for (const [pecaKey, pecaId] of Object.entries(existingPiecesForComponents)) {
const peca = pecas.find(p => `${p.of_number}-${p.etapa_fase}-${p.marca}` === pecaKey);
if (peca && peca.componentes.length > 0) {
// Filtrar apenas os componentes que ainda não existem
const componentsToFilter = [];
for (const componente of peca.componentes) {
const { data: existingComponent } = await supabase
.from('componentes_peca')
.select('id')
.eq('peca_id', pecaId)
.eq('marca_componente', componente.marca_componente.trim());
if (!existingComponent || existingComponent.length === 0) {
componentsToFilter.push(componente);
}
}
if (componentsToFilter.length > 0) {
const componentesData = componentsToFilter.map(comp => ({
peca_id: pecaId,
marca_componente: comp.marca_componente,
descricao: comp.descricao,
perfil: comp.perfil,
peso_unitario: comp.peso_unitario,
quantidade_por_peca: comp.quantidade_por_peca,
user_id: user.id
}));
console.log(`🔧 Inserindo ${componentesData.length} componentes adicionais...`);
const { error: componentesError } = await supabase
.from('componentes_peca')
.insert(componentesData);
if (componentesError) {
console.error('❌ Erro ao inserir componentes adicionais:', componentesError);
throw new Error(`Erro ao inserir componentes adicionais: ${componentesError.message}`);
}
totalComponentesInseridos += componentesData.length;
}
}
}
let successMessage = '';
if (totalPecasInseridas > 0 && totalComponentesInseridos > 0) {
successMessage = `${totalPecasInseridas} peça(s) e ${totalComponentesInseridos} componente(s) importados com sucesso!`;
} else if (totalPecasInseridas > 0) {
successMessage = `${totalPecasInseridas} peça(s) importada(s) com sucesso!`;
} else if (totalComponentesInseridos > 0) {
successMessage = `${totalComponentesInseridos} componente(s) adicionado(s) a peças existentes com sucesso!`;
} else {
successMessage = '️ Nenhuma peça nova foi importada. Todas as peças já existem no sistema.';
}
console.log('🎉 Importação concluída:', successMessage);
toast.success(successMessage);
onSuccess();
} catch (error) {
console.error('❌ Erro ao importar CSV completo:', error);
toast.error(`Erro ao importar arquivo CSV: ${error instanceof Error ? error.message : 'Erro desconhecido'}`);
} finally {
setImportLoading(false);
}
};
return {
importLoading,
exportCompleteTemplateCSV,
exportExampleDataCSV,
importCompleteCSV
};
}
+99
View File
@@ -0,0 +1,99 @@
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
export interface PecaExpedida {
id: string;
marca: string;
descricao: string;
peso_unitario: number;
quantidade_expedida: number;
quantidade_ja_apontada: number;
saldo_disponivel: number;
peca_id: string;
of_number: string;
}
export const usePecasExpedidas = (ofNumber: string) => {
return useQuery({
queryKey: ['pecas_expedidas', ofNumber],
queryFn: async () => {
if (!ofNumber) return [];
// Buscar peças expedidas nos romaneios
const { data: pecasExpedidas, error: errorExpedidas } = await supabase
.from('itens_romaneio_pecas')
.select(`
marca,
descricao,
peso_unitario,
quantidade_expedida,
peca_id,
romaneios_expedicao!inner(
of_number
)
`)
.eq('romaneios_expedicao.of_number', ofNumber);
if (errorExpedidas) throw errorExpedidas;
// Agrupar por marca e somar quantidades expedidas
const pecasAgrupadas = (pecasExpedidas || []).reduce((acc, item) => {
const key = item.marca;
if (!acc[key]) {
acc[key] = {
id: `${item.peca_id}_${key}`,
marca: item.marca,
descricao: item.descricao || '',
peso_unitario: item.peso_unitario,
quantidade_expedida: 0,
quantidade_ja_apontada: 0,
saldo_disponivel: 0,
peca_id: item.peca_id,
of_number: ofNumber
};
}
acc[key].quantidade_expedida += item.quantidade_expedida;
return acc;
}, {} as Record<string, PecaExpedida>);
// Buscar quantidades já apontadas no RDO
const { data: apontamentosRDO, error: errorRDO } = await supabase
.from('apontamentos_peca_obra')
.select(`
marca_peca,
quantidade,
diario_obra_rdo!inner(
of_number
)
`)
.eq('diario_obra_rdo.of_number', ofNumber);
if (errorRDO) throw errorRDO;
// Somar quantidades já apontadas por marca
const apontamentosAgrupados = (apontamentosRDO || []).reduce((acc, item) => {
const key = item.marca_peca;
acc[key] = (acc[key] || 0) + item.quantidade;
return acc;
}, {} as Record<string, number>);
// Calcular saldo disponível
const resultado = Object.values(pecasAgrupadas).map(peca => ({
...peca,
quantidade_ja_apontada: apontamentosAgrupados[peca.marca] || 0,
saldo_disponivel: peca.quantidade_expedida - (apontamentosAgrupados[peca.marca] || 0)
})).filter(peca => peca.quantidade_expedida > 0);
return resultado;
},
enabled: !!ofNumber,
});
};
export const usePecasComSaldo = (ofNumber: string) => {
const { data: pecasExpedidas = [] } = usePecasExpedidas(ofNumber);
return {
data: pecasExpedidas.filter(peca => peca.saldo_disponivel > 0),
isLoading: false
};
};
+68
View File
@@ -0,0 +1,68 @@
import { useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
export const usePecasP4Listener = () => {
useEffect(() => {
// Função para processar notificações de novas peças P4
const processarNovaPecaP4 = async (payload: any) => {
try {
const data = JSON.parse(payload);
console.log('🔔 Nova peça P4 detectada:', data);
// Verificar se a peça já existe na prioridade P4
const { data: itemExistente, error: errorCheck } = await supabase
.from('itens_prioridade_fabricacao')
.select('id')
.eq('peca_id', data.peca_id)
.eq('prioridade_fabricacao_id', data.prioridade_fabricacao_id)
.single();
if (errorCheck && errorCheck.code !== 'PGRST116') {
console.error('❌ Erro ao verificar item existente:', errorCheck);
return;
}
// Se não existe, criar o item na prioridade P4
if (!itemExistente) {
const { error: errorInsert } = await supabase
.from('itens_prioridade_fabricacao')
.insert({
peca_id: data.peca_id,
prioridade_fabricacao_id: data.prioridade_fabricacao_id,
quantidade_priorizada: data.quantidade,
peso_total: data.quantidade * data.peso_unitario,
ordem_fabricacao: 1
});
if (errorInsert) {
console.error('❌ Erro ao inserir peça na prioridade P4:', errorInsert);
} else {
console.log('✅ Peça adicionada à prioridade P4 automaticamente');
}
}
} catch (error) {
console.error('❌ Erro ao processar notificação de nova peça P4:', error);
}
};
// Escutar notificações do PostgreSQL
const channel = supabase
.channel('nova_peca_p4')
.on('postgres_changes', {
event: '*',
schema: 'public',
table: 'pecas'
}, (payload) => {
// Este listener não será usado pois estamos usando pg_notify
// Mantemos apenas como fallback
})
.subscribe();
// Cleanup
return () => {
supabase.removeChannel(channel);
};
}, []);
};
+178
View File
@@ -0,0 +1,178 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { Peca } from './usePecas';
export const usePecasParaPrioridade = () => {
const [pecasDisponiveis, setPecasDisponiveis] = useState<Peca[]>([]);
const [loading, setLoading] = useState(false);
const [ofNumbers, setOfNumbers] = useState<string[]>([]);
const [etapasFases, setEtapasFases] = useState<string[]>([]);
const fetchOFs = async () => {
try {
const { data, error } = await supabase
.from('pecas')
.select('of_number')
.not('of_number', 'is', null);
if (error) {
console.error('Erro ao buscar OFs:', error);
return;
}
const ofsUnicas = Array.from(new Set(data?.map(item => item.of_number).filter(Boolean) || []));
setOfNumbers(ofsUnicas);
} catch (error) {
console.error('Erro ao buscar OFs:', error);
}
};
const fetchEtapasFases = async (ofNumber: string) => {
try {
const { data, error } = await supabase
.from('pecas')
.select('etapa_fase')
.eq('of_number', ofNumber)
.not('etapa_fase', 'is', null);
if (error) {
console.error('Erro ao buscar fases:', error);
return;
}
const fasesUnicas = Array.from(new Set(data?.map(item => item.etapa_fase).filter(Boolean) || []));
setEtapasFases(fasesUnicas);
} catch (error) {
console.error('Erro ao buscar fases:', error);
}
};
const fetchPecasDisponiveis = async (ofNumber: string, etapaFase: string) => {
try {
setLoading(true);
console.log('🔍 Buscando peças disponíveis para OF:', ofNumber, 'Fase:', etapaFase);
// Buscar todas as peças da OF e fase especificadas
const { data: todasPecas, error: errorPecas } = await supabase
.from('pecas')
.select('*')
.eq('of_number', ofNumber)
.eq('etapa_fase', etapaFase);
if (errorPecas) {
console.error('❌ Erro ao buscar peças:', errorPecas);
setPecasDisponiveis([]);
return;
}
if (!todasPecas || todasPecas.length === 0) {
console.log('️ Nenhuma peça encontrada para esta OF e fase');
setPecasDisponiveis([]);
return;
}
// Buscar peças já priorizadas para esta OF e fase
const { data: pecasPriorizadas, error: errorPriorizadas } = await supabase
.from('prioridades_fabricacao')
.select(`
id,
itens_prioridade_fabricacao (
peca_id,
quantidade_priorizada
)
`)
.eq('of_number', ofNumber)
.eq('etapa_fase', etapaFase);
if (errorPriorizadas) {
console.error('❌ Erro ao buscar peças priorizadas:', errorPriorizadas);
setPecasDisponiveis([]);
return;
}
// Criar mapa de quantidades já priorizadas por peça
const quantidadesPriorizadas: { [pecaId: string]: number } = {};
if (pecasPriorizadas) {
pecasPriorizadas.forEach(prioridade => {
if (prioridade.itens_prioridade_fabricacao) {
prioridade.itens_prioridade_fabricacao.forEach((item: any) => {
if (item.peca_id) {
quantidadesPriorizadas[item.peca_id] =
(quantidadesPriorizadas[item.peca_id] || 0) + item.quantidade_priorizada;
}
});
}
});
}
console.log('📊 Quantidades priorizadas por peça:', quantidadesPriorizadas);
// Filtrar peças que ainda têm quantidade disponível
const pecasDisponiveis = todasPecas
.map(peca => {
const quantidadePriorizada = quantidadesPriorizadas[peca.id] || 0;
const quantidadeDisponivel = peca.quantidade - quantidadePriorizada;
return {
...peca,
quantidadeDisponivel: Math.max(0, quantidadeDisponivel)
};
})
.filter(peca => peca.quantidadeDisponivel > 0);
console.log(`✅ Encontradas ${pecasDisponiveis.length} peças com quantidade disponível`);
// Log detalhado das peças encontradas
pecasDisponiveis.forEach((peca: any) => {
const quantidadePriorizada = quantidadesPriorizadas[peca.id] || 0;
console.log(`📦 Peça ${peca.marca}: Total=${peca.quantidade}, Priorizada=${quantidadePriorizada}, Disponível=${peca.quantidadeDisponivel}`);
});
setPecasDisponiveis(pecasDisponiveis);
} catch (error) {
console.error('❌ Erro inesperado ao buscar peças:', error);
setPecasDisponiveis([]);
} finally {
setLoading(false);
}
};
const verificarPecaJaPriorizada = async (pecaId: string, prioridadeId: string) => {
try {
const { data, error } = await supabase
.from('itens_prioridade_fabricacao')
.select('id')
.eq('peca_id', pecaId)
.eq('prioridade_fabricacao_id', prioridadeId)
.single();
if (error && error.code !== 'PGRST116') {
console.error('Erro ao verificar peça:', error);
return false;
}
return !!data;
} catch (error) {
console.error('Erro ao verificar peça:', error);
return false;
}
};
useEffect(() => {
fetchOFs();
}, []);
return {
pecasDisponiveis,
ofNumbers,
etapasFases,
loading,
fetchOFs,
fetchEtapasFases,
fetchPecasDisponiveis,
verificarPecaJaPriorizada
};
};
+152
View File
@@ -0,0 +1,152 @@
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
export interface PecaPintura {
id: string;
marca: string;
etapa_fase: string;
descricao: string;
peso_unitario: number;
quantidade_disponivel: number;
of_number: string;
}
export const usePecasPintura = (ofNumber?: string, processoId?: string) => {
return useQuery({
queryKey: ['pecas-pintura', ofNumber, processoId],
queryFn: async (): Promise<PecaPintura[]> => {
if (!ofNumber || !processoId) {
console.log('Parâmetros obrigatórios não fornecidos:', { ofNumber, processoId });
return [];
}
console.log('Buscando peças para OF:', ofNumber, 'Processo:', processoId);
// Buscar informações do processo selecionado
const { data: processoAtual, error: processoError } = await supabase
.from('processos_fabricacao')
.select('nome')
.eq('id', processoId)
.single();
if (processoError) {
console.error('Erro ao buscar processo atual:', processoError);
throw processoError;
}
console.log('Processo atual encontrado:', processoAtual?.nome);
// Para Expedição, buscar peças que passaram por Pintura/Galv
let processosQuery = supabase
.from('processos_fabricacao')
.select('id')
.or('nome.ilike.%pintura%,nome.ilike.%galv%,nome.ilike.%galvanização%');
const { data: processosIds, error: processosError } = await processosQuery;
if (processosError) {
console.error('Erro ao buscar processos predecessores:', processosError);
throw processosError;
}
console.log('Processos de pintura/galv encontrados:', processosIds);
if (!processosIds || processosIds.length === 0) {
console.log('Nenhum processo de pintura/galvanização encontrado');
return [];
}
const processosIdsList = processosIds.map(p => p.id);
// Buscar peças que passaram pelos processos de pintura/galv
const { data: apontamentos, error: apontamentosError } = await supabase
.from('apontamentos_producao')
.select(`
peca_id,
quantidade_produzida,
pecas!inner(
id,
marca,
etapa_fase,
descricao,
peso_unitario,
of_number
)
`)
.eq('of_number', ofNumber)
.in('processo_id', processosIdsList)
.eq('tipo_apontamento', 'peca');
if (apontamentosError) {
console.error('Erro ao buscar apontamentos:', apontamentosError);
throw apontamentosError;
}
console.log('Apontamentos encontrados:', apontamentos?.length || 0);
// Buscar quantidades já expedidas em romaneios
const { data: itensExpedidos, error: expedidosError } = await supabase
.from('itens_romaneio_pecas')
.select(`
peca_id,
quantidade_expedida,
romaneios_expedicao!inner(of_number)
`)
.eq('romaneios_expedicao.of_number', ofNumber);
if (expedidosError) {
console.error('Erro ao buscar itens expedidos:', expedidosError);
throw expedidosError;
}
console.log('Itens já expedidos:', itensExpedidos?.length || 0);
// Agrupar por peça e calcular disponibilidade
const pecasMap = new Map<string, PecaPintura>();
// Processar apontamentos para calcular quantidade produzida total por peça
apontamentos?.forEach((apontamento) => {
const peca = apontamento.pecas;
if (!peca) return;
const key = peca.id;
if (pecasMap.has(key)) {
const existingPeca = pecasMap.get(key)!;
existingPeca.quantidade_disponivel += apontamento.quantidade_produzida;
} else {
pecasMap.set(key, {
id: peca.id,
marca: peca.marca || '',
etapa_fase: peca.etapa_fase || '',
descricao: peca.descricao || '',
peso_unitario: peca.peso_unitario || 0,
quantidade_disponivel: apontamento.quantidade_produzida,
of_number: peca.of_number
});
}
});
// Subtrair quantidades já expedidas
itensExpedidos?.forEach((item) => {
if (pecasMap.has(item.peca_id)) {
const peca = pecasMap.get(item.peca_id)!;
peca.quantidade_disponivel -= item.quantidade_expedida;
}
});
// Filtrar apenas peças com quantidade disponível > 0
const pecasDisponiveis = Array.from(pecasMap.values()).filter(
peca => peca.quantidade_disponivel > 0
);
console.log(`Peças disponíveis após filtro:`, pecasDisponiveis.length);
console.log('Peças encontradas:', pecasDisponiveis.map(p => `${p.marca} - ${p.etapa_fase} (${p.quantidade_disponivel})`));
return pecasDisponiveis;
},
enabled: !!ofNumber && !!processoId,
staleTime: 30000, // Cache por 30 segundos
refetchOnWindowFocus: false
});
};
+209
View File
@@ -0,0 +1,209 @@
import { useState, useMemo, useEffect } from 'react';
import { Peca } from './usePecas';
import { supabase } from '@/integrations/supabase/client';
import { applyMarcaFilter } from '@/utils/rangeFilter';
export type SortField = 'of_number' | 'etapa_fase' | 'marca' | 'descricao' | 'prioridade' | 'quantidade' | 'peso_unitario' | 'peso_total' | 'perfil_principal' | 'tem_componentes';
export type SortOrder = 'asc' | 'desc';
export const usePecasTable = (pecas: Peca[]) => {
const [ofFilter, setOfFilter] = useState('');
const [faseFilter, setFaseFilter] = useState('');
const [pecaFilter, setPecaFilter] = useState('');
const [descricaoFilter, setDescricaoFilter] = useState('');
const [priorityFilter, setPriorityFilter] = useState('all');
const [semComponentesFilter, setSemComponentesFilter] = useState('all');
const [sortField, setSortField] = useState<SortField>('of_number');
const [sortOrder, setSortOrder] = useState<SortOrder>('asc');
const [selectedPecas, setSelectedPecas] = useState<Set<string>>(new Set());
const [pecasWithComponente, setPecasWithComponente] = useState<Set<string>>(new Set());
// Limpar seleções quando a lista de peças muda (após exclusões)
useEffect(() => {
const currentIds = new Set(pecas.map(p => p.id));
const newSelectedPecas = new Set(
Array.from(selectedPecas).filter(id => currentIds.has(id))
);
// Atualizar seleções apenas se houver mudança
if (newSelectedPecas.size !== selectedPecas.size) {
setSelectedPecas(newSelectedPecas);
}
}, [pecas]);
const searchPecasByComponent = async (marca: string) => {
try {
const { data, error } = await supabase
.from('pecas')
.select('id')
.ilike('descricao', `%${marca}%`);
if (error) {
console.error('Erro ao buscar peças por componente:', error);
throw error;
}
if (data) {
const pecasIds = new Set(data.map(item => item.id));
setPecasWithComponente(pecasIds);
} else {
setPecasWithComponente(new Set());
}
} catch (error) {
console.error('Erro ao buscar peças por componente:', error);
throw error;
}
};
const filteredAndSortedPecas = useMemo(() => {
let filtered = pecas;
// Filtro por OF
if (ofFilter) {
const term = ofFilter.toLowerCase();
filtered = filtered.filter(peca =>
peca.of_number.toLowerCase().includes(term)
);
}
// Filtro por fase
if (faseFilter) {
const term = faseFilter.toLowerCase();
filtered = filtered.filter(peca =>
peca.etapa_fase?.toLowerCase().includes(term)
);
}
// Filtro por peça com suporte a range
if (pecaFilter) {
filtered = filtered.filter(peca =>
applyMarcaFilter(peca.marca, pecaFilter)
);
}
// Filtro por descrição
if (descricaoFilter) {
const term = descricaoFilter.toLowerCase();
filtered = filtered.filter(peca =>
peca.descricao?.toLowerCase().includes(term)
);
}
// Filtro por prioridade
if (priorityFilter !== 'all') {
filtered = filtered.filter(peca => peca.prioridade === priorityFilter);
}
// Filtro por "Sem Componentes" - usando tem_componentes com lógica invertida
if (semComponentesFilter !== 'all') {
if (semComponentesFilter === 'sim') {
filtered = filtered.filter(peca => peca.tem_componentes === false);
} else if (semComponentesFilter === 'nao') {
filtered = filtered.filter(peca => peca.tem_componentes === true);
}
}
// Ordenação
const sorted = [...filtered].sort((a, b) => {
const aValue = a[sortField];
const bValue = b[sortField];
// Tratamento especial para campo booleano tem_componentes
if (sortField === 'tem_componentes') {
const aBoolean = aValue === true;
const bBoolean = bValue === true;
if (aBoolean === bBoolean) return 0;
return sortOrder === 'asc' ?
(aBoolean ? 1 : -1) :
(bBoolean ? 1 : -1);
}
if (typeof aValue === 'string' && typeof bValue === 'string') {
const comparison = aValue.localeCompare(bValue);
return sortOrder === 'asc' ? comparison : -comparison;
}
if (typeof aValue === 'number' && typeof bValue === 'number') {
return sortOrder === 'asc' ? aValue - bValue : bValue - aValue;
}
return 0;
});
return sorted;
}, [pecas, ofFilter, faseFilter, pecaFilter, descricaoFilter, priorityFilter, semComponentesFilter, sortField, sortOrder]);
const handleSort = (field: SortField) => {
if (field === sortField) {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortOrder('asc');
}
};
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedPecas(new Set(filteredAndSortedPecas.map(p => p.id)));
} else {
setSelectedPecas(new Set());
}
};
const handleSelectPeca = (pecaId: string, checked: boolean) => {
const newSelected = new Set(selectedPecas);
if (checked) {
newSelected.add(pecaId);
} else {
newSelected.delete(pecaId);
}
setSelectedPecas(newSelected);
};
const selectAll = filteredAndSortedPecas.length > 0 &&
filteredAndSortedPecas.every(p => selectedPecas.has(p.id));
// Calcular peso total com base nas seleções
const pesoTotal = useMemo(() => {
let pecasParaCalcular: Peca[];
if (selectedPecas.size === 0) {
// Se nenhuma checkbox estiver habilitada, usar todas as peças filtradas
pecasParaCalcular = filteredAndSortedPecas;
} else {
// Se houver checkboxes habilitadas, usar apenas as peças selecionadas
pecasParaCalcular = filteredAndSortedPecas.filter(p => selectedPecas.has(p.id));
}
return pecasParaCalcular.reduce((total, peca) => {
return total + (peca.peso_total || 0);
}, 0);
}, [filteredAndSortedPecas, selectedPecas]);
return {
ofFilter,
setOfFilter,
faseFilter,
setFaseFilter,
pecaFilter,
setPecaFilter,
descricaoFilter,
setDescricaoFilter,
priorityFilter,
setPriorityFilter,
semComponentesFilter,
setSemComponentesFilter,
sortField,
sortOrder,
handleSort,
filteredAndSortedPecas,
selectedPecas,
selectAll,
handleSelectAll,
handleSelectPeca,
searchPecasByComponent,
pecasWithComponente,
pesoTotal
};
};
+32
View File
@@ -0,0 +1,32 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
export function usePecasWithComponents() {
const [pecasWithComponents, setPecasWithComponents] = useState<Set<string>>(new Set());
const loadPecasWithComponents = async () => {
try {
const { data, error } = await supabase
.from('componentes_peca')
.select('peca_id');
if (error) throw error;
const pecaIds = new Set<string>(data?.map(item => item.peca_id) || []);
setPecasWithComponents(pecaIds);
} catch (error) {
console.error('Erro ao carregar peças com componentes:', error);
}
};
useEffect(() => {
loadPecasWithComponents();
}, []);
return {
pecasWithComponents,
loadPecasWithComponents,
hasComponents: (pecaId: string) => pecasWithComponents.has(pecaId)
};
}
+105
View File
@@ -0,0 +1,105 @@
import { useUserPermissions } from '@/hooks/useUserPermissions';
import { useUserRole } from '@/hooks/useUserRole';
export function usePermissionControl() {
const { isAdmin } = useUserRole();
const { userPermissions, canPerformAction } = useUserPermissions();
// Verifica se pode visualizar (todos com qualquer permissão podem visualizar)
const canView = (): boolean => {
if (isAdmin) return true;
return Object.values(userPermissions).some(Boolean);
};
// Verifica se pode criar - visualizadores não podem
const canCreate = (): boolean => {
if (isAdmin) return true;
if (userPermissions.can_view_only && !userPermissions.can_create_only && !userPermissions.can_create_update_delete) return false;
return canPerformAction('create');
};
// Verifica se pode editar - visualizadores não podem
const canEdit = (): boolean => {
if (isAdmin) return true;
if (userPermissions.can_view_only && !userPermissions.can_create_update_delete) return false;
return canPerformAction('update');
};
// Verifica se pode excluir - visualizadores não podem
const canDelete = (): boolean => {
if (isAdmin) return true;
if (userPermissions.can_view_only && !userPermissions.can_create_update_delete) return false;
return canPerformAction('delete');
};
// Verifica se pode fazer operações administrativas - visualizadores não podem
const canAdmin = (): boolean => {
if (isAdmin) return true;
if (userPermissions.can_view_only && !userPermissions.can_admin) return false;
return canPerformAction('admin');
};
// Verifica se pode importar/exportar - visualizadores não podem
const canImportExport = (): boolean => {
if (isAdmin) return true;
if (userPermissions.can_view_only && !userPermissions.can_create_only && !userPermissions.can_create_update_delete) return false;
return canPerformAction('create');
};
// Verifica se pode acessar ferramentas - visualizadores não podem
const canAccessTools = (): boolean => {
if (isAdmin) return true;
if (userPermissions.can_view_only && !userPermissions.can_create_only && !userPermissions.can_create_update_delete) return false;
return true;
};
// Verifica se pode ter ações limitadas em Tarefas, Sistema e Sugestões - visualizadores têm acesso limitado
const canInteractWithSpecialMenus = (): boolean => {
if (isAdmin) return true;
if (userPermissions.can_view_only && !userPermissions.can_create_only && !userPermissions.can_create_update_delete) return false;
return true;
};
// Obtém o nível de permissão do usuário
const getPermissionLevel = (): 'admin' | 'full' | 'create' | 'view' | 'none' => {
if (isAdmin || userPermissions.can_admin) return 'admin';
if (userPermissions.can_create_update_delete) return 'full';
if (userPermissions.can_create_only) return 'create';
if (userPermissions.can_view_only) return 'view';
return 'none';
};
// Verifica se botão/ação deve ser desabilitado
const isDisabled = (action: 'create' | 'edit' | 'delete' | 'admin' | 'import' | 'export'): boolean => {
switch (action) {
case 'create':
return !canCreate();
case 'edit':
return !canEdit();
case 'delete':
return !canDelete();
case 'admin':
return !canAdmin();
case 'import':
case 'export':
return !canImportExport();
default:
return false;
}
};
return {
canView,
canCreate,
canEdit,
canDelete,
canAdmin,
canImportExport,
canAccessTools,
canInteractWithSpecialMenus,
getPermissionLevel,
isDisabled,
userPermissions
};
}
+77
View File
@@ -0,0 +1,77 @@
import { useUserPermissions } from '@/hooks/useUserPermissions';
import { useUserRole } from '@/hooks/useUserRole';
import type { ResourceKey, PermissionLevel } from '@/hooks/useUserPermissions/types';
export function usePermissions() {
const { isAdmin } = useUserRole();
const { hasAccess, userPermissions, getResourcePermission, canPerformActionByResource } = useUserPermissions();
const checkResourceAccess = (resourceKey?: string): boolean => {
if (import.meta.env.DEV) {
console.log('🔍 checkResourceAccess called:', { resourceKey, isAdmin, result: hasAccess(resourceKey) });
}
return hasAccess(resourceKey);
};
const getPermissionForResource = (resourceKey: string, permissions: Record<string, boolean>): PermissionLevel | null => {
// Admin sempre tem permissão total
if (isAdmin) return 'can_admin';
// Use resource-specific permission if available
const resourcePermission = getResourcePermission(resourceKey as ResourceKey);
if (resourcePermission && resourcePermission !== 'no_access') {
return resourcePermission;
}
// Se não tem acesso básico, retorna null
if (!hasAccess()) return null;
// Verifica as permissões funcionais do usuário como fallback
if (permissions.can_admin) return 'can_admin';
if (permissions.can_create_update_delete) return 'can_create_update_delete';
if (permissions.can_create_only) return 'can_create_only';
if (permissions.can_view_only) return 'can_view_only';
// Por padrão, apenas visualização
return 'can_view_only';
};
const canPerformAction = (
action: 'create' | 'read' | 'update' | 'delete' | 'admin',
resourceKey: string,
permissions: Record<string, boolean>
): boolean => {
// Try resource-specific permission first
if (canPerformActionByResource(action, resourceKey as ResourceKey)) {
return true;
}
// Fallback to functional permissions
const permission = getPermissionForResource(resourceKey, permissions);
if (!permission) return false;
switch (permission) {
case 'can_admin':
return true;
case 'can_create_update_delete':
return action !== 'admin';
case 'can_create_only':
return action === 'create' || action === 'read';
case 'can_view_only':
return action === 'read';
case 'no_access':
return false;
default:
return false;
}
};
return {
checkResourceAccess,
getPermissionForResource,
canPerformAction,
userPermissions
};
}
+82
View File
@@ -0,0 +1,82 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
export interface PrioridadeConfig {
id: string;
codigo: string;
nome: string;
cor: string;
ordem: number;
ativo: boolean;
created_at: string;
updated_at: string;
}
export const usePrioridades = () => {
const [prioridades, setPrioridades] = useState<PrioridadeConfig[]>([]);
const [loading, setLoading] = useState(true);
const fetchPrioridades = async () => {
try {
const { data, error } = await supabase
.from('prioridades_config')
.select('*')
.eq('ativo', true)
.order('ordem');
if (error) {
console.error('Erro ao buscar prioridades:', error);
toast.error('Erro ao carregar prioridades');
return;
}
setPrioridades(data || []);
} catch (error) {
console.error('Erro ao buscar prioridades:', error);
toast.error('Erro ao carregar prioridades');
} finally {
setLoading(false);
}
};
const updatePrioridade = async (id: string, updates: Partial<PrioridadeConfig>) => {
try {
const { error } = await supabase
.from('prioridades_config')
.update(updates)
.eq('id', id);
if (error) {
console.error('Erro ao atualizar prioridade:', error);
toast.error('Erro ao atualizar prioridade');
return false;
}
toast.success('Prioridade atualizada com sucesso!');
await fetchPrioridades();
return true;
} catch (error) {
console.error('Erro ao atualizar prioridade:', error);
toast.error('Erro ao atualizar prioridade');
return false;
}
};
const getPrioridadeByCodigo = (codigo: string) => {
return prioridades.find(p => p.codigo === codigo);
};
useEffect(() => {
fetchPrioridades();
}, []);
return {
prioridades,
loading,
updatePrioridade,
getPrioridadeByCodigo,
refetch: fetchPrioridades
};
};
+175
View File
@@ -0,0 +1,175 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
import { useAuth } from '@/hooks/useAuth';
export interface PrioridadeFabricacao {
id: string;
of_number: string;
etapa_fase: string;
prioridade_id: string;
nome_prioridade: string;
ativo: boolean;
created_at: string;
updated_at: string;
created_by: string;
prioridade_config?: {
codigo: string;
nome: string;
cor: string;
};
}
export interface ItemPrioridade {
id: string;
prioridade_fabricacao_id: string;
peca_id: string;
quantidade_priorizada: number;
ordem_fabricacao: number;
peso_total: number;
created_at: string;
updated_at: string;
peca?: {
marca: string;
descricao: string;
peso_unitario: number;
quantidade: number;
};
}
export const usePrioridadesFabricacao = () => {
const { user } = useAuth();
const [prioridades, setPrioridades] = useState<PrioridadeFabricacao[]>([]);
const [loading, setLoading] = useState(true);
const fetchPrioridades = async (ofNumber?: string, etapaFase?: string) => {
try {
setLoading(true);
let query = supabase
.from('prioridades_fabricacao')
.select(`
*,
prioridade_config:prioridades_config(codigo, nome, cor)
`)
.eq('ativo', true)
.order('created_at', { ascending: false });
if (ofNumber) {
query = query.eq('of_number', ofNumber);
}
if (etapaFase) {
query = query.eq('etapa_fase', etapaFase);
}
const { data, error } = await query;
if (error) {
console.error('Erro ao buscar prioridades:', error);
toast.error('Erro ao carregar prioridades');
return;
}
setPrioridades(data || []);
} catch (error) {
console.error('Erro ao buscar prioridades:', error);
toast.error('Erro ao carregar prioridades');
} finally {
setLoading(false);
}
};
const criarPrioridade = async (dadosPrioridade: Omit<PrioridadeFabricacao, 'id' | 'created_at' | 'updated_at' | 'created_by'>) => {
try {
if (!user) {
toast.error('Usuário não autenticado');
return null;
}
const { data, error } = await supabase
.from('prioridades_fabricacao')
.insert([{
...dadosPrioridade,
created_by: user.id
}])
.select()
.single();
if (error) {
console.error('Erro ao criar prioridade:', error);
toast.error('Erro ao criar prioridade');
return null;
}
toast.success('Prioridade criada com sucesso!');
await fetchPrioridades();
return data;
} catch (error) {
console.error('Erro ao criar prioridade:', error);
toast.error('Erro ao criar prioridade');
return null;
}
};
const atualizarPrioridade = async (id: string, dadosAtualizacao: Partial<PrioridadeFabricacao>) => {
try {
const { error } = await supabase
.from('prioridades_fabricacao')
.update(dadosAtualizacao)
.eq('id', id);
if (error) {
console.error('Erro ao atualizar prioridade:', error);
toast.error('Erro ao atualizar prioridade');
return false;
}
toast.success('Prioridade atualizada com sucesso!');
await fetchPrioridades();
return true;
} catch (error) {
console.error('Erro ao atualizar prioridade:', error);
toast.error('Erro ao atualizar prioridade');
return false;
}
};
const deletarPrioridade = async (id: string) => {
try {
const { error } = await supabase
.from('prioridades_fabricacao')
.update({ ativo: false })
.eq('id', id);
if (error) {
console.error('Erro ao deletar prioridade:', error);
toast.error('Erro ao deletar prioridade');
return false;
}
toast.success('Prioridade removida com sucesso!');
await fetchPrioridades();
return true;
} catch (error) {
console.error('Erro ao deletar prioridade:', error);
toast.error('Erro ao deletar prioridade');
return false;
}
};
useEffect(() => {
fetchPrioridades();
}, []);
return {
prioridades,
loading,
fetchPrioridades,
criarPrioridade,
atualizarPrioridade,
deletarPrioridade,
refetch: fetchPrioridades
};
};
+186
View File
@@ -0,0 +1,186 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
interface ChartData {
name: string;
weight: number;
}
interface ProcessChartData {
corte: ChartData[];
solda: ChartData[];
montagem: ChartData[];
}
export const useProcessChartData = (ofNumber: string) => {
const [chartData, setChartData] = useState<ProcessChartData>({
corte: [],
solda: [],
montagem: []
});
const [loading, setLoading] = useState(false);
const calculateProcessData = async (processName: string): Promise<ChartData[]> => {
try {
console.log(`Buscando dados para processo: ${processName}`);
// Buscar processo por nome
const { data: processo, error: processoError } = await supabase
.from('processos_fabricacao')
.select('id')
.ilike('nome', `%${processName}%`)
.single();
if (processoError || !processo) {
console.log(`Processo ${processName} não encontrado:`, processoError);
return [
{ name: '7-15d', weight: 0 },
{ name: 'ult.7d', weight: 0 },
{ name: 'prev.7d', weight: 0 }
];
}
console.log(`Processo ${processName} encontrado com ID:`, processo.id);
const hoje = new Date();
const seteDiasAtras = new Date(hoje);
seteDiasAtras.setDate(hoje.getDate() - 7);
const quatorzeDiasAtras = new Date(hoje);
quatorzeDiasAtras.setDate(hoje.getDate() - 14);
const seisDiasAtras = new Date(hoje);
seisDiasAtras.setDate(hoje.getDate() - 6);
// Buscar apontamentos dos últimos 14 dias
const { data: apontamentos, error: apontamentosError } = await supabase
.from('apontamentos_producao')
.select(`
*,
peca:pecas(peso_unitario),
componente:componentes_peca(peso_unitario)
`)
.eq('of_number', ofNumber)
.eq('processo_id', processo.id)
.gte('data_apontamento', quatorzeDiasAtras.toISOString().split('T')[0])
.lte('data_apontamento', hoje.toISOString().split('T')[0]);
if (apontamentosError) {
console.error(`Erro ao buscar apontamentos para ${processName}:`, apontamentosError);
return [
{ name: '7-15d', weight: 0 },
{ name: 'ult.7d', weight: 0 },
{ name: 'prev.7d', weight: 0 }
];
}
console.log(`Apontamentos encontrados para ${processName}:`, apontamentos?.length || 0);
if (!apontamentos || apontamentos.length === 0) {
return [
{ name: '7-15d', weight: 0 },
{ name: 'ult.7d', weight: 0 },
{ name: 'prev.7d', weight: 0 }
];
}
// Calcular pesos por período
let peso7a14Dias = 0;
let peso6DiasRecentes = 0;
apontamentos.forEach(apontamento => {
let pesoUnitario = 0;
if (apontamento.tipo_apontamento === 'componente' && apontamento.componente?.peso_unitario) {
pesoUnitario = apontamento.componente.peso_unitario;
} else if (apontamento.tipo_apontamento === 'peca' && apontamento.peca?.peso_unitario) {
pesoUnitario = apontamento.peca.peso_unitario;
}
const pesoTotal = pesoUnitario * apontamento.quantidade_produzida;
const dataApontamento = new Date(apontamento.data_apontamento);
// Período de 7-14 dias atrás
if (dataApontamento >= quatorzeDiasAtras && dataApontamento < seteDiasAtras) {
peso7a14Dias += pesoTotal;
}
// Últimos 6 dias
if (dataApontamento >= seisDiasAtras && dataApontamento <= hoje) {
peso6DiasRecentes += pesoTotal;
}
});
// Calcular peso necessário para próximos 7 dias (meta)
const { data: ofData } = await supabase
.from('ordens_fabricacao')
.select('peso_total, data_prazo')
.eq('num_of', ofNumber)
.single();
let pesoMeta = 0;
if (ofData?.peso_total && ofData?.data_prazo) {
const dataFim = new Date(ofData.data_prazo);
const diasRestantes = Math.max(1, Math.ceil((dataFim.getTime() - hoje.getTime()) / (1000 * 60 * 60 * 24)));
const pesoTotalProcessado = peso7a14Dias + peso6DiasRecentes;
const pesoRestante = Math.max(0, ofData.peso_total - pesoTotalProcessado);
// Meta para próximos 7 dias baseada no ritmo necessário
if (diasRestantes <= 7) {
pesoMeta = pesoRestante;
} else {
pesoMeta = (pesoRestante / diasRestantes) * 7;
}
}
const resultado = [
{ name: '7-15d', weight: Number(peso7a14Dias.toFixed(0)) }, // Mantido em Kg
{ name: 'ult.7d', weight: Number(peso6DiasRecentes.toFixed(0)) },
{ name: 'prev.7d', weight: Number(pesoMeta.toFixed(0)) }
];
console.log(`Dados calculados para ${processName}:`, resultado);
return resultado;
} catch (error) {
console.error(`Erro ao calcular dados do processo ${processName}:`, error);
return [
{ name: '7-15d', weight: 0 },
{ name: 'ult.7d', weight: 0 },
{ name: 'prev.7d', weight: 0 }
];
}
};
const fetchChartData = async () => {
if (!ofNumber) return;
console.log('Iniciando busca de dados dos gráficos para OF:', ofNumber);
setLoading(true);
try {
const [corteData, soldaData, montagemData] = await Promise.all([
calculateProcessData('corte'),
calculateProcessData('solda'),
calculateProcessData('montagem')
]);
const newChartData = {
corte: corteData,
solda: soldaData,
montagem: montagemData
};
console.log('Dados finais dos gráficos:', newChartData);
setChartData(newChartData);
} catch (error) {
console.error('Erro ao buscar dados dos gráficos:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchChartData();
}, [ofNumber]);
return { chartData, loading, refetch: fetchChartData };
};
+67
View File
@@ -0,0 +1,67 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
export const useProcessosDebug = () => {
const [processos, setProcessos] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const verificarProcessos = async () => {
setLoading(true);
setError(null);
try {
console.log('🔍 DEBUG: Verificando processos na tabela...');
// Verificar se a tabela existe e tem dados
const { data: allProcessos, error: allError, count } = await supabase
.from('processos_fabricacao')
.select('*', { count: 'exact' });
if (allError) {
console.error('❌ DEBUG: Erro ao buscar todos os processos:', allError);
setError(allError.message);
return;
}
console.log(`📊 DEBUG: Total de processos na tabela: ${count}`);
console.log('📋 DEBUG: Todos os processos:', allProcessos);
// Verificar processos ativos
const { data: processosAtivos, error: ativosError } = await supabase
.from('processos_fabricacao')
.select('*')
.eq('ativo', true)
.order('ordem');
if (ativosError) {
console.error('❌ DEBUG: Erro ao buscar processos ativos:', ativosError);
setError(ativosError.message);
return;
}
console.log(`✅ DEBUG: Processos ativos encontrados: ${processosAtivos?.length || 0}`);
console.log('📋 DEBUG: Processos ativos:', processosAtivos);
setProcessos(processosAtivos || []);
} catch (err) {
console.error('❌ DEBUG: Erro inesperado:', err);
setError('Erro inesperado ao verificar processos');
} finally {
setLoading(false);
}
};
useEffect(() => {
verificarProcessos();
}, []);
return {
processos,
loading,
error,
verificarProcessos
};
};
+41
View File
@@ -0,0 +1,41 @@
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
export interface ProcessoFabricacao {
id: string;
nome: string;
descricao?: string;
ordem: number;
ativo: boolean;
cor?: string;
}
export const useProcessosFabricacao = () => {
return useQuery({
queryKey: ['processos-fabricacao'],
queryFn: async (): Promise<ProcessoFabricacao[]> => {
console.log('🔍 Buscando processos de fabricação...');
const { data, error } = await supabase
.from('processos_fabricacao')
.select('*')
.eq('ativo', true)
.order('ordem');
if (error) {
console.error('❌ Erro ao buscar processos de fabricação:', error);
throw error;
}
console.log('✅ Processos carregados:', data?.length || 0);
console.log('📋 Processos disponíveis:', data?.map(p => ({ id: p.id, nome: p.nome, ordem: p.ordem })));
return data || [];
},
staleTime: 300000, // Cache por 5 minutos
refetchOnWindowFocus: false,
retry: 3,
retryDelay: 1000
});
};
+56
View File
@@ -0,0 +1,56 @@
import { useState } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
export function useProfileImage() {
const [updating, setUpdating] = useState(false);
const updateProfileImage = async (userId: string, imageUrl: string) => {
try {
setUpdating(true);
const { error } = await supabase
.from('profiles')
.update({ profile_image_url: imageUrl })
.eq('id', userId);
if (error) throw error;
return true;
} catch (error) {
console.error('Error updating profile image:', error);
toast.error('Erro ao atualizar foto de perfil');
return false;
} finally {
setUpdating(false);
}
};
const removeProfileImage = async (userId: string) => {
try {
setUpdating(true);
const { error } = await supabase
.from('profiles')
.update({ profile_image_url: null })
.eq('id', userId);
if (error) throw error;
return true;
} catch (error) {
console.error('Error removing profile image:', error);
toast.error('Erro ao remover foto de perfil');
return false;
} finally {
setUpdating(false);
}
};
return {
updating,
updateProfileImage,
removeProfileImage
};
}
+123
View File
@@ -0,0 +1,123 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
interface Prompt {
id: string;
name: string;
content: string;
created_at: string;
updated_at: string;
created_by: string;
}
export const usePrompts = () => {
const [prompts, setPrompts] = useState<Prompt[]>([]);
const [loading, setLoading] = useState(true);
const fetchPrompts = async () => {
try {
const { data, error } = await supabase
.from('prompts')
.select('*')
.order('created_at', { ascending: false });
if (error) throw error;
setPrompts(data || []);
} catch (error) {
console.error('Erro ao carregar prompts:', error);
toast.error('Erro ao carregar prompts');
}
};
const savePrompt = async (prompt: Partial<Prompt>) => {
try {
const { data: userData } = await supabase.auth.getUser();
if (prompt.id) {
const { error } = await supabase
.from('prompts')
.update({
name: prompt.name,
content: prompt.content
})
.eq('id', prompt.id);
if (error) throw error;
toast.success('Prompt atualizado com sucesso!');
} else {
const { error } = await supabase
.from('prompts')
.insert({
name: prompt.name,
content: prompt.content,
created_by: userData.user?.id
});
if (error) throw error;
toast.success('Prompt criado com sucesso!');
}
await fetchPrompts();
} catch (error) {
console.error('Erro ao salvar prompt:', error);
toast.error('Erro ao salvar prompt');
}
};
const deletePrompt = async (id: string) => {
try {
const { error } = await supabase
.from('prompts')
.delete()
.eq('id', id);
if (error) throw error;
toast.success('Prompt excluído com sucesso!');
await fetchPrompts();
} catch (error) {
console.error('Erro ao excluir prompt:', error);
toast.error('Erro ao excluir prompt');
}
};
const downloadPromptAsJson = (prompt: Prompt, filename?: string) => {
const jsonData = {
name: prompt.name,
content: prompt.content,
created_at: prompt.created_at,
updated_at: prompt.updated_at
};
const dataStr = JSON.stringify(jsonData, null, 2);
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
const exportFileDefaultName = filename || `${prompt.name.replace(/\s+/g, '_')}.json`;
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', exportFileDefaultName);
linkElement.click();
};
useEffect(() => {
const loadData = async () => {
setLoading(true);
await fetchPrompts();
setLoading(false);
};
loadData();
}, []);
return {
prompts,
loading,
savePrompt,
deletePrompt,
downloadPromptAsJson,
refreshPrompts: fetchPrompts
};
};
+156
View File
@@ -0,0 +1,156 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
// Types for Unproductive Time Appointments
export interface ApontamentoImprodutivo {
id: string;
rdo_id: string;
motivo_id: string;
hora_inicio: string; // TIME format from database
hora_fim: string; // TIME format from database
duracao_total?: string; // INTERVAL from database
descricao?: string | null;
created_at: string;
motivos_improdutivos?: {
id: string;
motivo: string;
descricao: string | null;
categoria: 'Cliente' | 'Empresa Montadora' | 'Contratada' | 'Terceiros Indiretos';
};
}
export interface NovoApontamentoImprodutivo {
rdo_id: string;
motivo_id: string;
hora_inicio: string;
hora_fim: string;
descricao?: string;
}
// Hook para buscar apontamentos improdutivos de um RDO específico
export const useApontamentosImprodutivos = (rdoId?: string) => {
return useQuery({
queryKey: ['apontamentos_improdutivos', rdoId],
queryFn: async () => {
if (!rdoId) return [];
const { data, error } = await supabase
.from('apontamentos_improdutivos')
.select(`
*,
motivos_improdutivos:motivo_id(*)
`)
.eq('rdo_id', rdoId)
.order('hora_inicio', { ascending: true });
if (error) throw error;
return data as ApontamentoImprodutivo[];
},
enabled: !!rdoId,
});
};
// Hook para criar apontamento improdutivo
export const useCreateApontamentoImprodutivo = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (apontamento: NovoApontamentoImprodutivo) => {
const { data, error } = await supabase
.from('apontamentos_improdutivos')
.insert([apontamento])
.select(`
*,
motivos_improdutivos:motivo_id(*)
`)
.single();
if (error) throw error;
return data;
},
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: ['apontamentos_improdutivos', variables.rdo_id]
});
toast.success('Apontamento improdutivo criado com sucesso!');
},
onError: (error: any) => {
toast.error('Erro ao criar apontamento improdutivo: ' + error.message);
},
});
};
// Hook para atualizar apontamento improdutivo
export const useUpdateApontamentoImprodutivo = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, ...updates }: Partial<ApontamentoImprodutivo> & { id: string }) => {
const { data, error } = await supabase
.from('apontamentos_improdutivos')
.update(updates)
.eq('id', id)
.select(`
*,
motivos_improdutivos:motivo_id(*)
`)
.single();
if (error) throw error;
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['apontamentos_improdutivos'] });
toast.success('Apontamento improdutivo atualizado com sucesso!');
},
onError: (error: any) => {
toast.error('Erro ao atualizar apontamento improdutivo: ' + error.message);
},
});
};
// Hook para deletar apontamento improdutivo
export const useDeleteApontamentoImprodutivo = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (id: string) => {
const { error } = await supabase
.from('apontamentos_improdutivos')
.delete()
.eq('id', id);
if (error) throw error;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['apontamentos_improdutivos'] });
toast.success('Apontamento improdutivo removido com sucesso!');
},
onError: (error: any) => {
toast.error('Erro ao remover apontamento improdutivo: ' + error.message);
},
});
};
// Utility function to calculate duration between two times
export const calculateDuration = (horaInicio: string, horaFim: string): string => {
try {
const inicio = new Date(`2000-01-01T${horaInicio}`);
const fim = new Date(`2000-01-01T${horaFim}`);
// Handle cases where end time is on the next day
if (fim < inicio) {
fim.setDate(fim.getDate() + 1);
}
const diffMs = fim.getTime() - inicio.getTime();
const hours = Math.floor(diffMs / (1000 * 60 * 60));
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
} catch (error) {
console.error('Error calculating duration:', error);
return '00:00';
}
};
+129
View File
@@ -0,0 +1,129 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
// Types for Resources Appointments
export interface ApontamentoRecursoObra {
id: string;
rdo_id: string;
recurso_id: string;
horas_trabalhadas: number;
created_at: string;
recursos_obra?: {
id: string;
tipo_recurso: string;
nome_recurso: string;
descricao: string | null;
};
}
export interface NovoApontamentoRecurso {
rdo_id: string;
recurso_id: string;
horas_trabalhadas: number;
}
// Hook para buscar apontamentos de recursos de um RDO específico
export const useApontamentosRecursosObra = (rdoId?: string) => {
return useQuery({
queryKey: ['apontamentos_recursos_obra', rdoId],
queryFn: async () => {
if (!rdoId) return [];
const { data, error } = await supabase
.from('apontamentos_recursos_obra')
.select(`
*,
recursos_obra:recurso_id(*)
`)
.eq('rdo_id', rdoId)
.order('created_at', { ascending: true });
if (error) throw error;
return data as ApontamentoRecursoObra[];
},
enabled: !!rdoId,
});
};
// Hook para criar apontamento de recurso
export const useCreateApontamentoRecurso = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (apontamento: NovoApontamentoRecurso) => {
const { data, error } = await supabase
.from('apontamentos_recursos_obra')
.insert([apontamento])
.select(`
*,
recursos_obra:recurso_id(*)
`)
.single();
if (error) throw error;
return data;
},
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: ['apontamentos_recursos_obra', variables.rdo_id]
});
toast.success('Apontamento de recurso criado com sucesso!');
},
onError: (error: any) => {
toast.error('Erro ao criar apontamento de recurso: ' + error.message);
},
});
};
// Hook para atualizar apontamento de recurso
export const useUpdateApontamentoRecurso = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, ...updates }: Partial<ApontamentoRecursoObra> & { id: string }) => {
const { data, error } = await supabase
.from('apontamentos_recursos_obra')
.update(updates)
.eq('id', id)
.select(`
*,
recursos_obra:recurso_id(*)
`)
.single();
if (error) throw error;
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['apontamentos_recursos_obra'] });
toast.success('Apontamento de recurso atualizado com sucesso!');
},
onError: (error: any) => {
toast.error('Erro ao atualizar apontamento de recurso: ' + error.message);
},
});
};
// Hook para deletar apontamento de recurso
export const useDeleteApontamentoRecurso = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (id: string) => {
const { error } = await supabase
.from('apontamentos_recursos_obra')
.delete()
.eq('id', id);
if (error) throw error;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['apontamentos_recursos_obra'] });
toast.success('Apontamento de recurso removido com sucesso!');
},
onError: (error: any) => {
toast.error('Erro ao remover apontamento de recurso: ' + error.message);
},
});
};
+183
View File
@@ -0,0 +1,183 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
export interface RastreabilidadeMaterial {
id: string;
lote: string;
material_id: string;
quantidade: number;
data_entrada?: string;
fornecedor?: string;
certificado?: string;
corrida?: string;
data_validade?: string;
nota_fiscal?: string;
status: string;
created_at: string;
created_by?: string;
estoque_materiais?: {
codigo: string;
descricao: string;
qualidade_aco?: string;
};
}
// Função para gerar número de lote com a regra especificada
const generateLoteNumber = async (): Promise<string> => {
try {
// Buscar o próximo número sequencial
const { data: lastLote, error } = await supabase
.from('rastreabilidade_materiais')
.select('lote')
.like('lote', 'RE%')
.order('created_at', { ascending: false })
.limit(1)
.single();
let nextSequential = 1; // Começar com 001
if (!error && lastLote) {
// Extrair número sequencial do último lote (primeiros 3 dígitos após "RE")
const match = lastLote.lote.match(/^RE(\d{3})/);
if (match) {
const lastSequential = parseInt(match[1]);
nextSequential = (lastSequential + 1) % 1000; // Cicla de 0 a 999
}
}
// Gerar componentes do lote
const now = new Date();
const month = String(now.getMonth() + 1).padStart(2, '0'); // 01-12
const year = String(now.getFullYear()).slice(-2); // Últimos 2 dígitos do ano
const sequential = String(nextSequential).padStart(3, '0'); // 001-999
return `RE${sequential}${month}${year}`;
} catch (error) {
console.error('Erro ao gerar número do lote:', error);
// Fallback para caso de erro
const now = new Date();
const month = String(now.getMonth() + 1).padStart(2, '0');
const year = String(now.getFullYear()).slice(-2);
return `RE001${month}${year}`;
}
};
export const useRastreabilidadeMateriais = () => {
return useQuery({
queryKey: ['rastreabilidade-materiais'],
queryFn: async () => {
const { data, error } = await supabase
.from('rastreabilidade_materiais')
.select(`
*,
estoque_materiais (
codigo,
descricao,
qualidade_aco
)
`)
.order('created_at', { ascending: false });
if (error) throw error;
return data as RastreabilidadeMaterial[];
},
});
};
export const useCriarRastreabilidade = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (rastreabilidade: Partial<RastreabilidadeMaterial>) => {
// Gerar lote automaticamente se não fornecido ou for null
let loteNumber = rastreabilidade.lote;
if (!loteNumber || loteNumber.trim() === '') {
loteNumber = await generateLoteNumber();
}
const { data, error } = await supabase
.from('rastreabilidade_materiais')
.insert([{
lote: loteNumber,
material_id: rastreabilidade.material_id,
quantidade: rastreabilidade.quantidade,
data_entrada: rastreabilidade.data_entrada,
fornecedor: rastreabilidade.fornecedor,
certificado: rastreabilidade.certificado,
corrida: rastreabilidade.corrida,
data_validade: rastreabilidade.data_validade,
nota_fiscal: rastreabilidade.nota_fiscal,
status: rastreabilidade.status || 'Ativo',
created_by: (await supabase.auth.getUser()).data.user?.id
}])
.select()
.single();
if (error) throw error;
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['rastreabilidade-materiais'] });
toast.success('Lote criado com sucesso!');
},
onError: (error) => {
console.error('Erro ao criar lote:', error);
toast.error('Erro ao criar lote');
},
});
};
export const useAtualizarRastreabilidade = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, ...rastreabilidade }: Partial<RastreabilidadeMaterial> & { id: string }) => {
const { data, error } = await supabase
.from('rastreabilidade_materiais')
.update(rastreabilidade)
.eq('id', id)
.select()
.single();
if (error) throw error;
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['rastreabilidade-materiais'] });
toast.success('Lote atualizado com sucesso!');
},
onError: (error) => {
console.error('Erro ao atualizar lote:', error);
toast.error('Erro ao atualizar lote');
},
});
};
export const useExcluirRastreabilidade = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (id: string) => {
const { error } = await supabase
.from('rastreabilidade_materiais')
.delete()
.eq('id', id);
if (error) throw error;
return id;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['rastreabilidade-materiais'] });
toast.success('Lote excluído com sucesso!');
},
onError: (error) => {
console.error('Erro ao excluir lote:', error);
toast.error('Erro ao excluir lote');
},
});
};
// Re-exportar hook do estoque para uso no modal
export { useEstoqueMateriais } from './useEstoque';
+167
View File
@@ -0,0 +1,167 @@
import React, { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
import { formatBrazilianDateTime } from '@/utils/dateTimeUtils';
interface ApontamentoDiario {
id: string;
of_number: string;
etapa_fase: string;
marca: string;
descricao: string;
processo: string;
quantidade: number;
peso_unitario: number;
peso_total: number;
data_apontamento: string;
created_at: string;
}
interface ResumoProcesso {
processo: string;
quantidade: number;
apontamentos: number;
peso_total: number;
}
export const useRelatorioDiario = (selectedDate: string) => {
const [apontamentos, setApontamentos] = useState<ApontamentoDiario[]>([]);
const [loading, setLoading] = useState(false);
const fetchApontamentos = async () => {
if (!selectedDate) return;
setLoading(true);
try {
console.log('Buscando apontamentos para data:', selectedDate);
const { data: apontamentosData, error } = await supabase
.from('apontamentos_producao')
.select(`
id,
of_number,
quantidade_produzida,
data_apontamento,
created_at,
tipo_apontamento,
peca:pecas(
id,
marca,
descricao,
etapa_fase,
peso_unitario
),
componente:componentes_peca(
id,
marca_componente,
descricao,
peso_unitario
),
processo:processos_fabricacao(
nome
)
`)
.eq('data_apontamento', selectedDate)
.order('of_number')
.order('created_at');
if (error) {
console.error('Erro ao buscar apontamentos:', error);
throw error;
}
console.log('Apontamentos encontrados:', apontamentosData?.length || 0);
console.log('Data de busca usada:', selectedDate);
const apontamentosFormatados: ApontamentoDiario[] = (apontamentosData || []).map(apt => {
// Determinar dados baseado no tipo de apontamento
let marca, descricao, etapa_fase, peso_unitario;
if (apt.tipo_apontamento === 'componente' && apt.componente) {
marca = apt.componente.marca_componente;
descricao = apt.componente.descricao || 'Componente';
etapa_fase = '0'; // Componentes não têm etapa/fase
peso_unitario = Number(apt.componente.peso_unitario || 0);
} else if (apt.tipo_apontamento === 'peca' && apt.peca) {
marca = apt.peca.marca;
descricao = apt.peca.descricao || 'Peça';
etapa_fase = apt.peca.etapa_fase || '0';
peso_unitario = Number(apt.peca.peso_unitario || 0);
} else {
marca = 'N/A';
descricao = 'N/A';
etapa_fase = '0';
peso_unitario = 0;
}
const peso_total = peso_unitario * apt.quantidade_produzida;
return {
id: apt.id,
of_number: apt.of_number,
etapa_fase,
marca,
descricao,
processo: apt.processo?.nome || 'N/A',
quantidade: apt.quantidade_produzida,
peso_unitario,
peso_total,
data_apontamento: apt.data_apontamento,
created_at: apt.created_at
};
});
// Ordenar por OF, etapa_fase, marca
apontamentosFormatados.sort((a, b) => {
if (a.of_number !== b.of_number) {
return a.of_number.localeCompare(b.of_number);
}
if (a.etapa_fase !== b.etapa_fase) {
return a.etapa_fase.localeCompare(b.etapa_fase);
}
return a.marca.localeCompare(b.marca);
});
setApontamentos(apontamentosFormatados);
console.log('Apontamentos formatados:', apontamentosFormatados.length);
} catch (error) {
console.error('Erro ao buscar dados do relatório diário:', error);
toast.error('Erro ao carregar relatório diário');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchApontamentos();
}, [selectedDate]);
// Calcular resumo por processo
const resumoProcessos: ResumoProcesso[] = React.useMemo(() => {
const resumo = new Map<string, { quantidade: number; apontamentos: number; peso_total: number }>();
apontamentos.forEach(apt => {
const processo = apt.processo;
if (!resumo.has(processo)) {
resumo.set(processo, { quantidade: 0, apontamentos: 0, peso_total: 0 });
}
const dados = resumo.get(processo)!;
dados.quantidade += apt.quantidade;
dados.apontamentos += 1;
dados.peso_total += apt.peso_total;
});
return Array.from(resumo.entries())
.map(([processo, dados]) => ({ processo, ...dados }))
.sort((a, b) => b.quantidade - a.quantidade);
}, [apontamentos]);
return {
apontamentos,
resumoProcessos,
loading,
refetch: fetchApontamentos
};
};
+203
View File
@@ -0,0 +1,203 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
export interface PecaComStatus {
id: string;
of_number: string;
etapa_fase: string;
marca: string;
quantidade: number;
peso_unitario: number;
peso_total: number;
tem_componentes: boolean;
processos: {
corte: boolean;
solda: boolean;
pintura: boolean;
expedicao: boolean;
};
}
export interface ResumoProcesso {
processo: string;
pesoTotal: number;
quantidadePecas: number;
}
// Função para ordenação numérica natural
const naturalSort = (a: string, b: string): number => {
const collator = new Intl.Collator(undefined, {
numeric: true,
sensitivity: 'base'
});
return collator.compare(a, b);
};
export const useRelatorioPecasProcesso = (ofNumber: string) => {
const [pecasComStatus, setPecasComStatus] = useState<PecaComStatus[]>([]);
const [resumoProcessos, setResumoProcessos] = useState<ResumoProcesso[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchPecasComStatus = async () => {
if (!ofNumber) {
setPecasComStatus([]);
setResumoProcessos([]);
return;
}
setLoading(true);
try {
console.log('Buscando peças e status para OF:', ofNumber);
// Buscar peças da OF incluindo tem_componentes
const { data: pecasData, error: pecasError } = await supabase
.from('pecas')
.select('id, of_number, etapa_fase, marca, quantidade, peso_unitario, peso_total, tem_componentes')
.eq('of_number', ofNumber);
if (pecasError) {
console.error('Erro ao buscar peças:', pecasError);
throw pecasError;
}
if (!pecasData || pecasData.length === 0) {
console.log('Nenhuma peça encontrada para OF:', ofNumber);
setPecasComStatus([]);
setResumoProcessos([]);
return;
}
console.log('Peças encontradas:', pecasData.length);
// Buscar processos e seus IDs - incluindo variações de nome
const { data: processosData, error: processosError } = await supabase
.from('processos_fabricacao')
.select('id, nome')
.in('nome', ['Corte', 'Solda', 'Pintura/Galvanizacao', 'Pintura/Galv', 'Pintura', 'Expedicao']);
if (processosError) {
console.error('Erro ao buscar processos:', processosError);
throw processosError;
}
const processosMap = new Map(processosData?.map(p => [p.nome, p.id]) || []);
console.log('Processos mapeados:', Object.fromEntries(processosMap));
// Buscar apontamentos para todas as peças da OF
const pecaIds = pecasData.map(p => p.id);
const { data: apontamentosData, error: apontamentosError } = await supabase
.from('apontamentos_producao')
.select(`
peca_id,
processo_id,
quantidade_produzida,
processos_fabricacao!inner(nome)
`)
.eq('of_number', ofNumber)
.in('peca_id', pecaIds);
if (apontamentosError) {
console.error('Erro ao buscar apontamentos:', apontamentosError);
throw apontamentosError;
}
console.log('Apontamentos encontrados:', apontamentosData?.length || 0);
// Mapear status dos processos por peça
const statusPorPeca = new Map<string, Set<string>>();
apontamentosData?.forEach(apt => {
if (!statusPorPeca.has(apt.peca_id)) {
statusPorPeca.set(apt.peca_id, new Set());
}
statusPorPeca.get(apt.peca_id)!.add(apt.processos_fabricacao.nome);
});
// Construir array de peças com status
const pecasComStatusFormatadas: PecaComStatus[] = pecasData.map(peca => {
const processosRealizados = statusPorPeca.get(peca.id) || new Set();
return {
id: peca.id,
of_number: peca.of_number,
etapa_fase: peca.etapa_fase || '',
marca: peca.marca,
quantidade: peca.quantidade || 0,
peso_unitario: Number(peca.peso_unitario || 0),
peso_total: Number(peca.peso_total || 0),
tem_componentes: peca.tem_componentes || false,
processos: {
corte: processosRealizados.has('Corte'),
solda: processosRealizados.has('Solda'),
pintura: processosRealizados.has('Pintura/Galvanizacao') ||
processosRealizados.has('Pintura/Galv') ||
processosRealizados.has('Pintura'),
expedicao: processosRealizados.has('Expedicao')
}
};
});
// Ordenar peças por etapa_fase e marca com ordenação numérica natural
pecasComStatusFormatadas.sort((a, b) => {
const faseComparison = naturalSort(a.etapa_fase, b.etapa_fase);
if (faseComparison !== 0) return faseComparison;
return naturalSort(a.marca, b.marca);
});
console.log('Peças com status formatadas:', pecasComStatusFormatadas.length);
// Calcular resumo por processo
const resumo: ResumoProcesso[] = [
{
processo: 'Corte',
pesoTotal: pecasComStatusFormatadas
.filter(p => p.processos.corte)
.reduce((sum, p) => sum + p.peso_total, 0),
quantidadePecas: pecasComStatusFormatadas.filter(p => p.processos.corte).length
},
{
processo: 'Solda',
pesoTotal: pecasComStatusFormatadas
.filter(p => p.processos.solda)
.reduce((sum, p) => sum + p.peso_total, 0),
quantidadePecas: pecasComStatusFormatadas.filter(p => p.processos.solda).length
},
{
processo: 'Pintura/Galvanização',
pesoTotal: pecasComStatusFormatadas
.filter(p => p.processos.pintura)
.reduce((sum, p) => sum + p.peso_total, 0),
quantidadePecas: pecasComStatusFormatadas.filter(p => p.processos.pintura).length
},
{
processo: 'Expedição',
pesoTotal: pecasComStatusFormatadas
.filter(p => p.processos.expedicao)
.reduce((sum, p) => sum + p.peso_total, 0),
quantidadePecas: pecasComStatusFormatadas.filter(p => p.processos.expedicao).length
}
];
setPecasComStatus(pecasComStatusFormatadas);
setResumoProcessos(resumo);
} catch (error) {
console.error('Erro ao buscar dados do relatório de peças por processo:', error);
toast.error('Erro ao carregar relatório de peças por processo');
} finally {
setLoading(false);
}
};
fetchPecasComStatus();
}, [ofNumber]);
return {
pecasComStatus,
resumoProcessos,
loading
};
};
+27
View File
@@ -0,0 +1,27 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
export const useRemoverItemInsumo = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (itemId: string) => {
const { error } = await supabase
.from('itens_romaneio_insumos')
.delete()
.eq('id', itemId);
if (error) throw error;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['romaneios'] });
toast.success('Insumo removido com sucesso!');
},
onError: (error) => {
console.error('Erro ao remover insumo:', error);
toast.error('Erro ao remover insumo');
},
});
};
+28
View File
@@ -0,0 +1,28 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
export const useRemoverItemPeca = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (itemId: string) => {
const { error } = await supabase
.from('itens_romaneio_pecas')
.delete()
.eq('id', itemId);
if (error) throw error;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['romaneios'] });
queryClient.invalidateQueries({ queryKey: ['pecas-pintura'] });
toast.success('Item removido com sucesso!');
},
onError: (error) => {
console.error('Erro ao remover item:', error);
toast.error('Erro ao remover item');
},
});
};
+44
View File
@@ -0,0 +1,44 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
export const useRemoverRomaneio = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (romaneioId: string) => {
// Primeiro, remover todos os itens de peças do romaneio
const { error: pecasError } = await supabase
.from('itens_romaneio_pecas')
.delete()
.eq('romaneio_id', romaneioId);
if (pecasError) throw pecasError;
// Depois, remover todos os itens de insumos do romaneio
const { error: insumosError } = await supabase
.from('itens_romaneio_insumos')
.delete()
.eq('romaneio_id', romaneioId);
if (insumosError) throw insumosError;
// Por fim, remover o romaneio
const { error: romaneioError } = await supabase
.from('romaneios_expedicao')
.delete()
.eq('id', romaneioId);
if (romaneioError) throw romaneioError;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['romaneios'] });
toast.success('Romaneio removido com sucesso!');
},
onError: (error) => {
console.error('Erro ao remover romaneio:', error);
toast.error('Erro ao remover romaneio');
},
});
};
+410
View File
@@ -0,0 +1,410 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
export interface RomaneioExpedicao {
id: string;
numero_romaneio: string;
of_number: string;
data_romaneio: string;
data_criacao: string;
revisao: number;
motivo_revisao?: string;
data_prevista_entrega?: string;
prioridade: string;
status: string;
observacoes?: string;
peso_total_romaneio: number;
previsao_kg?: number;
maior_dimensao?: string;
tipo_transporte?: string;
frete_tipo?: string;
nome_motorista?: string;
created_at: string;
updated_at: string;
created_by?: string;
itens_pecas?: ItemRomaneioPeca[];
itens_insumos?: ItemRomaneioInsumo[];
}
export interface ItemRomaneioPeca {
id: string;
romaneio_id: string;
peca_id: string;
marca: string;
fase?: string;
descricao?: string;
comprimento?: number;
peso_unitario: number;
quantidade_expedida: number;
peso_total: number;
quantidade_faltante: number;
created_at: string;
}
export interface ItemRomaneioInsumo {
id: string;
romaneio_id: string;
tipo_insumo: string;
descricao: string;
unidade: string;
quantidade_expedida: number;
peso_unitario?: number;
peso_total?: number;
observacoes?: string;
created_at: string;
}
export interface PecaPintura {
id: string;
marca: string;
etapa_fase?: string;
descricao?: string;
peso_unitario: number;
quantidade_produzida_pintura: number;
quantidade_ja_expedida: number;
quantidade_disponivel: number;
of_number: string;
}
export const useRomaneios = (ofNumberFilter?: string) => {
return useQuery({
queryKey: ['romaneios', ofNumberFilter],
queryFn: async () => {
let query = supabase
.from('romaneios_expedicao')
.select(`
*,
itens_pecas:itens_romaneio_pecas(*),
itens_insumos:itens_romaneio_insumos(*)
`)
.order('created_at', { ascending: false });
if (ofNumberFilter && ofNumberFilter !== 'all') {
query = query.eq('of_number', ofNumberFilter);
}
const { data, error } = await query;
if (error) throw error;
// Calcular peso total correto das peças para cada romaneio
const normalizedData = data?.map(romaneio => {
const pesoTotalPecas = romaneio.itens_pecas?.reduce((sum: number, item: any) => sum + (item.peso_total || 0), 0) || 0;
const pesoTotalInsumos = romaneio.itens_insumos?.reduce((sum: number, item: any) => sum + (item.peso_total || 0), 0) || 0;
return {
...romaneio,
peso_total_romaneio: pesoTotalPecas + pesoTotalInsumos,
prioridade: romaneio.prioridade === 'Normal' || romaneio.prioridade === 'Urgente'
? romaneio.prioridade
: 'Normal',
status: ['Em planejamento', 'Confirmado', 'Expedido', 'Entregue', 'Conferido em Obra'].includes(romaneio.status)
? romaneio.status
: 'Em planejamento',
frete_tipo: romaneio.frete_tipo === 'terceiros' || romaneio.frete_tipo === 'proprio'
? romaneio.frete_tipo
: undefined
};
});
return normalizedData as RomaneioExpedicao[];
},
});
};
export const usePecasPintura = (ofNumber?: string) => {
return useQuery({
queryKey: ['pecas-pintura', ofNumber],
queryFn: async () => {
if (!ofNumber) return [];
// Buscar peças que passaram pelo processo de pintura
const { data: apontamentosPintura, error: apontamentosError } = await supabase
.from('apontamentos_producao')
.select(`
peca_id,
quantidade_produzida,
pecas (
id,
marca,
etapa_fase,
descricao,
peso_unitario,
of_number
),
processos_fabricacao (
nome
)
`)
.eq('of_number', ofNumber)
.ilike('processos_fabricacao.nome', '%pintura%');
if (apontamentosError) throw apontamentosError;
// Buscar quantidades já expedidas
const { data: itensExpedidos, error: expedidosError } = await supabase
.from('itens_romaneio_pecas')
.select('peca_id, quantidade_expedida')
.in('peca_id', apontamentosPintura?.map(a => a.peca_id) || []);
if (expedidosError) throw expedidosError;
// Calcular disponibilidade
const pecasPintura: PecaPintura[] = [];
const pecasMap = new Map();
apontamentosPintura?.forEach(apontamento => {
const pecaId = apontamento.peca_id;
const peca = apontamento.pecas;
if (!peca) return;
if (pecasMap.has(pecaId)) {
pecasMap.get(pecaId).quantidade_produzida_pintura += apontamento.quantidade_produzida;
} else {
pecasMap.set(pecaId, {
id: peca.id,
marca: peca.marca,
etapa_fase: peca.etapa_fase,
descricao: peca.descricao,
peso_unitario: peca.peso_unitario,
quantidade_produzida_pintura: apontamento.quantidade_produzida,
of_number: peca.of_number
});
}
});
pecasMap.forEach((peca, pecaId) => {
const totalExpedido = itensExpedidos
?.filter(item => item.peca_id === pecaId)
.reduce((sum, item) => sum + item.quantidade_expedida, 0) || 0;
pecasPintura.push({
...peca,
quantidade_ja_expedida: totalExpedido,
quantidade_disponivel: peca.quantidade_produzida_pintura - totalExpedido
});
});
return pecasPintura.filter(p => p.quantidade_disponivel > 0);
},
enabled: !!ofNumber,
});
};
const generateRomaneioNumber = (ofNumber: string, existingRomaneios: RomaneioExpedicao[]) => {
// Extrair apenas o número da OF (sem o B)
const ofNumOnly = ofNumber.replace('B', '');
// Contar romaneios existentes para esta OF
const existingCount = existingRomaneios.filter(r => r.of_number === ofNumber).length;
const nextSequence = existingCount + 1;
return `RO${ofNumOnly}-${nextSequence.toString().padStart(2, '0')}`;
};
export const useCriarRomaneio = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (romaneio: Omit<RomaneioExpedicao, 'id' | 'numero_romaneio' | 'created_at' | 'updated_at'>) => {
const { data: existingRomaneios } = await supabase
.from('romaneios_expedicao')
.select('*')
.eq('of_number', romaneio.of_number);
const numeroRomaneio = generateRomaneioNumber(romaneio.of_number, existingRomaneios || []);
const { data, error } = await supabase
.from('romaneios_expedicao')
.insert([{
numero_romaneio: numeroRomaneio,
of_number: romaneio.of_number,
data_romaneio: romaneio.data_romaneio,
data_criacao: romaneio.data_criacao,
revisao: romaneio.revisao || 1,
motivo_revisao: romaneio.motivo_revisao,
data_prevista_entrega: romaneio.data_prevista_entrega,
prioridade: romaneio.prioridade,
status: romaneio.status,
observacoes: romaneio.observacoes,
peso_total_romaneio: romaneio.peso_total_romaneio,
previsao_kg: romaneio.previsao_kg,
maior_dimensao: romaneio.maior_dimensao,
tipo_transporte: romaneio.tipo_transporte,
frete_tipo: romaneio.frete_tipo,
nome_motorista: romaneio.nome_motorista,
created_by: (await supabase.auth.getUser()).data.user?.id
}])
.select()
.single();
if (error) throw error;
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['romaneios'] });
toast.success('Romaneio criado com sucesso!');
},
onError: (error) => {
console.error('Erro ao criar romaneio:', error);
toast.error('Erro ao criar romaneio');
},
});
};
export const useAtualizarRomaneio = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, ...romaneio }: Partial<RomaneioExpedicao> & { id: string }) => {
// Buscar status atual antes da atualização
const { data: romaneioAtual, error: errorAtual } = await supabase
.from('romaneios_expedicao')
.select('status, numero_romaneio, of_number')
.eq('id', id)
.single();
if (errorAtual) {
console.error('❌ Erro ao buscar romaneio atual:', errorAtual);
throw errorAtual;
}
console.log('📋 Status atual do romaneio:', romaneioAtual?.status);
console.log('📋 Novo status a ser definido:', romaneio.status);
const { data, error } = await supabase
.from('romaneios_expedicao')
.update(romaneio)
.eq('id', id)
.select(`
*,
itens_pecas:itens_romaneio_pecas(*)
`)
.single();
if (error) throw error;
// VERIFICAÇÃO ESPECÍFICA: Disparar apontamento automático APENAS quando status muda PARA "Entregue"
const statusAnterior = romaneioAtual?.status;
const novoStatus = romaneio.status;
console.log('🔍 Verificando mudança de status:');
console.log('- Status anterior:', statusAnterior);
console.log('- Novo status:', novoStatus);
if (novoStatus === 'Entregue' && statusAnterior !== 'Entregue') {
console.log('🚀 CONDIÇÃO ATENDIDA: Status mudou PARA "Entregue" - iniciando apontamento automático');
console.log('📦 Dados do romaneio atualizado:', data);
// Buscar dados completos do romaneio incluindo itens
const { data: romaneioCompleto, error: errorCompleto } = await supabase
.from('romaneios_expedicao')
.select(`
*,
itens_pecas:itens_romaneio_pecas(*)
`)
.eq('id', id)
.single();
if (errorCompleto) {
console.error('❌ Erro ao buscar dados completos do romaneio:', errorCompleto);
return data;
}
console.log('📋 Dados completos do romaneio para apontamento:', romaneioCompleto);
console.log('🔢 Número de peças no romaneio:', romaneioCompleto?.itens_pecas?.length || 0);
// Verificar se há peças para apontar
if (!romaneioCompleto?.itens_pecas || romaneioCompleto.itens_pecas.length === 0) {
console.log('⚠️ Romaneio sem peças - não será processado apontamento automático');
return data;
}
// Disparar evento customizado para apontamento automático
const eventDetail = {
romaneioId: id,
status: novoStatus,
romaneioData: romaneioCompleto,
statusAnterior: statusAnterior
};
console.log('🎯 Disparando evento romaneio-entregue para apontamento automático:', eventDetail);
// Usar setTimeout para garantir que o evento seja disparado após a atualização
setTimeout(() => {
window.dispatchEvent(new CustomEvent('romaneio-entregue', {
detail: eventDetail
}));
}, 100);
} else if (novoStatus === 'Entregue' && statusAnterior === 'Entregue') {
console.log('️ Status já era "Entregue" - apontamento automático não será disparado');
} else {
console.log(`️ Status atualizado de "${statusAnterior}" para "${novoStatus}" - apontamento automático não será disparado (só para mudança PARA "Entregue")`);
}
return data;
},
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['romaneios'] });
toast.success('Romaneio atualizado com sucesso!');
},
onError: (error) => {
console.error('Erro ao atualizar romaneio:', error);
toast.error('Erro ao atualizar romaneio');
},
});
};
export const useAdicionarItemPeca = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (item: Omit<ItemRomaneioPeca, 'id' | 'created_at'>) => {
const { data, error } = await supabase
.from('itens_romaneio_pecas')
.insert([item])
.select()
.single();
if (error) throw error;
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['romaneios'] });
toast.success('Item adicionado com sucesso!');
},
onError: (error) => {
console.error('Erro ao adicionar item:', error);
toast.error('Erro ao adicionar item');
},
});
};
export const useAdicionarItemInsumo = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (item: Omit<ItemRomaneioInsumo, 'id' | 'created_at'>) => {
const { data, error } = await supabase
.from('itens_romaneio_insumos')
.insert([item])
.select()
.single();
if (error) throw error;
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['romaneios'] });
toast.success('Insumo adicionado com sucesso!');
},
onError: (error) => {
console.error('Erro ao adicionar insumo:', error);
toast.error('Erro ao adicionar insumo');
},
});
};
+345
View File
@@ -0,0 +1,345 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { useAuth } from './useAuth';
import { logger } from '@/utils/logger';
interface SessionLog {
id: string;
user_id: string;
session_start: string;
session_end: string | null;
duration_minutes: number | null;
ip_address: string | null;
user_agent: string | null;
is_active: boolean;
created_at: string;
updated_at: string;
}
interface OnlineUser {
user_id: string;
email: string;
full_name: string;
avatar_url: string;
session_start: string;
}
interface SessionStats {
total_sessions: number;
total_users: number;
average_duration: number;
active_sessions: number;
}
export const useSessionLogs = () => {
const [sessionLogs, setSessionLogs] = useState<SessionLog[]>([]);
const [onlineUsers, setOnlineUsers] = useState<OnlineUser[]>([]);
const [sessionStats, setSessionStats] = useState<SessionStats | null>(null);
const [loading, setLoading] = useState(false);
const [currentSessionId, setCurrentSessionId] = useState<string | null>(null);
const { user } = useAuth();
// Iniciar sessão quando o usuário faz login
const startSession = async () => {
if (!user) return;
try {
logger.info('Starting user session');
// Finalizar sessões ativas anteriores (caso houve falha no logout)
await supabase
.from('user_session_logs')
.update({
is_active: false,
session_end: new Date().toISOString(),
duration_minutes: 0
})
.eq('user_id', user.id)
.eq('is_active', true);
// Criar nova sessão
const { data, error } = await supabase
.from('user_session_logs')
.insert({
user_id: user.id,
ip_address: null, // Pode ser obtido do frontend se necessário
user_agent: navigator.userAgent,
is_active: true
})
.select()
.single();
if (error) {
logger.error('Error starting session:', error);
throw error;
}
setCurrentSessionId(data.id);
// Armazenar ID da sessão no localStorage para persistência
localStorage.setItem('currentSessionId', data.id);
logger.success('Session started successfully');
} catch (error) {
logger.error('Erro ao iniciar sessão:', error);
}
};
// Finalizar sessão quando o usuário faz logout
const endSession = async () => {
const sessionId = currentSessionId || localStorage.getItem('currentSessionId');
if (!sessionId) return;
try {
logger.info('Ending user session');
await supabase.rpc('end_user_session', { session_id: sessionId });
setCurrentSessionId(null);
localStorage.removeItem('currentSessionId');
logger.success('Session ended successfully');
} catch (error) {
logger.error('Erro ao finalizar sessão:', error);
}
};
// Buscar logs de sessão
const fetchSessionLogs = async (filters?: {
userId?: string;
startDate?: string;
endDate?: string;
}) => {
setLoading(true);
try {
logger.info('Fetching session logs with filters:', filters);
let query = supabase
.from('user_session_logs')
.select('*')
.order('session_start', { ascending: false });
if (filters?.userId) {
query = query.eq('user_id', filters.userId);
}
if (filters?.startDate) {
query = query.gte('session_start', filters.startDate);
}
if (filters?.endDate) {
query = query.lte('session_start', filters.endDate);
}
const { data, error } = await query;
if (error) {
logger.error('Error fetching session logs:', error);
throw error;
}
setSessionLogs(data as SessionLog[] || []);
logger.success('Session logs fetched successfully');
} catch (error) {
logger.error('Erro ao buscar logs de sessão:', error);
} finally {
setLoading(false);
}
};
// Buscar usuários online
const fetchOnlineUsers = async () => {
try {
logger.info('Fetching online users');
const { data, error } = await supabase.rpc('get_online_users');
if (error) {
logger.error('Error fetching online users:', error);
throw error;
}
setOnlineUsers(data || []);
logger.success('Online users fetched successfully');
} catch (error) {
logger.error('Erro ao buscar usuários online:', error);
}
};
// Buscar estatísticas de sessão
const fetchSessionStats = async (filters?: {
userId?: string;
period?: 'day' | 'week' | 'month';
}) => {
try {
logger.info('Fetching session stats with filters:', filters);
let query = supabase
.from('user_session_logs')
.select('*');
if (filters?.userId) {
query = query.eq('user_id', filters.userId);
}
// Filtrar por período
if (filters?.period) {
const now = new Date();
let startDate;
switch (filters.period) {
case 'day':
startDate = new Date(now.setDate(now.getDate() - 1));
break;
case 'week':
startDate = new Date(now.setDate(now.getDate() - 7));
break;
case 'month':
startDate = new Date(now.setMonth(now.getMonth() - 1));
break;
}
if (startDate) {
query = query.gte('session_start', startDate.toISOString());
}
}
const { data, error } = await query;
if (error) {
logger.error('Error fetching session stats:', error);
throw error;
}
const stats: SessionStats = {
total_sessions: data?.length || 0,
total_users: new Set(data?.map(log => log.user_id)).size || 0,
average_duration: data?.reduce((acc, log) => acc + (log.duration_minutes || 0), 0) / (data?.length || 1) || 0,
active_sessions: data?.filter(log => log.is_active).length || 0
};
setSessionStats(stats);
logger.success('Session stats fetched successfully');
} catch (error) {
logger.error('Erro ao buscar estatísticas:', error);
}
};
// Exportar relatório
const exportReport = async (format: 'csv' | 'json', filters?: {
userId?: string;
startDate?: string;
endDate?: string;
}) => {
try {
logger.info(`Exporting report in ${format} format`);
let query = supabase
.from('user_session_logs')
.select('*')
.order('session_start', { ascending: false });
if (filters?.userId) {
query = query.eq('user_id', filters.userId);
}
if (filters?.startDate) {
query = query.gte('session_start', filters.startDate);
}
if (filters?.endDate) {
query = query.lte('session_start', filters.endDate);
}
const { data, error } = await query;
if (error) {
logger.error('Error exporting report:', error);
throw error;
}
// Buscar informações dos usuários separadamente
const userIds = Array.from(new Set(data?.map(log => log.user_id) || []));
const { data: users } = await supabase
.from('profiles')
.select('id, email, full_name')
.in('id', userIds);
const userMap = new Map(users?.map(user => [user.id, user]) || []);
if (format === 'csv') {
const csv = [
'Email,Nome,Início da Sessão,Fim da Sessão,Duração (min),Status',
...(data || []).map(log => {
const user = userMap.get(log.user_id);
return `${user?.email || ''},${user?.full_name || ''},${log.session_start},${log.session_end || ''},${log.duration_minutes || ''},${log.is_active ? 'Ativa' : 'Finalizada'}`;
})
].join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `relatorio-sessoes-${new Date().toISOString().split('T')[0]}.csv`;
a.click();
URL.revokeObjectURL(url);
} else {
const enrichedData = data?.map(log => ({
...log,
user_info: userMap.get(log.user_id)
}));
const json = JSON.stringify(enrichedData, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `relatorio-sessoes-${new Date().toISOString().split('T')[0]}.json`;
a.click();
URL.revokeObjectURL(url);
}
logger.success('Report exported successfully');
} catch (error) {
logger.error('Erro ao exportar relatório:', error);
}
};
// Atualizar automaticamente usuários online
useEffect(() => {
fetchOnlineUsers();
const interval = setInterval(fetchOnlineUsers, 30000); // Atualizar a cada 30 segundos
return () => clearInterval(interval);
}, []);
// Gerenciar sessão baseado no estado de autenticação
useEffect(() => {
if (user && !currentSessionId && !localStorage.getItem('currentSessionId')) {
startSession();
} else if (!user && currentSessionId) {
endSession();
}
}, [user]);
// Finalizar sessão quando a janela é fechada
useEffect(() => {
const handleBeforeUnload = () => {
const sessionId = currentSessionId || localStorage.getItem('currentSessionId');
if (sessionId) {
// Para navegadores modernos, usar navigator.sendBeacon se disponível
if (navigator.sendBeacon) {
const payload = JSON.stringify({ session_id: sessionId });
navigator.sendBeacon('/api/end-session', payload);
}
}
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [currentSessionId]);
return {
sessionLogs,
onlineUsers,
sessionStats,
loading,
currentSessionId,
startSession,
endSession,
fetchSessionLogs,
fetchOnlineUsers,
fetchSessionStats,
exportReport
};
};
+171
View File
@@ -0,0 +1,171 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { useAuth } from './useAuth';
import { toast } from 'sonner';
interface SessionLog {
id: string;
user_id: string;
session_start: string;
session_end: string | null;
duration_minutes: number | null;
is_active: boolean;
created_at: string;
}
interface OnlineUser {
user_id: string;
email: string;
full_name: string;
avatar_url: string;
session_start: string;
}
export const useSessionLogsSimple = () => {
const [sessionLogs, setSessionLogs] = useState<SessionLog[]>([]);
const [onlineUsers, setOnlineUsers] = useState<OnlineUser[]>([]);
const [loading, setLoading] = useState(false);
const { user } = useAuth();
// Buscar logs de sessão
const fetchSessionLogs = async (filters?: {
userId?: string;
startDate?: string;
endDate?: string;
}) => {
if (!user) return;
setLoading(true);
try {
let query = supabase
.from('user_session_logs')
.select('*')
.order('session_start', { ascending: false })
.limit(100);
if (filters?.userId && filters.userId !== 'all') {
query = query.eq('user_id', filters.userId);
}
if (filters?.startDate) {
query = query.gte('session_start', filters.startDate);
}
if (filters?.endDate) {
query = query.lte('session_start', filters.endDate);
}
const { data, error } = await query;
if (error) {
console.error('Error fetching session logs:', error);
toast.error('Erro ao carregar logs de sessão');
return;
}
setSessionLogs(data || []);
} catch (error) {
console.error('Error fetching session logs:', error);
toast.error('Erro ao carregar logs de sessão');
} finally {
setLoading(false);
}
};
// Buscar usuários online
const fetchOnlineUsers = async () => {
if (!user) return;
try {
const { data, error } = await supabase.rpc('get_online_users');
if (error) {
console.error('Error fetching online users:', error);
return;
}
setOnlineUsers(data || []);
} catch (error) {
console.error('Error fetching online users:', error);
}
};
// Exportar relatório CSV
const exportToCsv = async (filters?: {
userId?: string;
startDate?: string;
endDate?: string;
}) => {
if (!user) return;
try {
let query = supabase
.from('user_session_logs')
.select('*')
.order('session_start', { ascending: false });
if (filters?.userId && filters.userId !== 'all') {
query = query.eq('user_id', filters.userId);
}
if (filters?.startDate) {
query = query.gte('session_start', filters.startDate);
}
if (filters?.endDate) {
query = query.lte('session_start', filters.endDate);
}
const { data, error } = await query;
if (error) {
toast.error('Erro ao exportar relatório');
return;
}
// Buscar informações dos usuários
const userIds = Array.from(new Set(data?.map(log => log.user_id) || []));
const { data: users } = await supabase
.from('profiles')
.select('id, email, full_name')
.in('id', userIds);
const userMap = new Map(users?.map(user => [user.id, user]) || []);
// Gerar CSV
const csvHeader = 'Email,Nome,Início da Sessão,Fim da Sessão,Duração (min),Status\n';
const csvRows = (data || []).map(log => {
const user = userMap.get(log.user_id);
const duration = log.duration_minutes ? Math.round(log.duration_minutes) : '';
const status = log.is_active ? 'Ativa' : 'Finalizada';
return `${user?.email || ''},${user?.full_name || ''},${log.session_start},${log.session_end || ''},${duration},${status}`;
}).join('\n');
const csv = csvHeader + csvRows;
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `logs-sessao-${new Date().toISOString().split('T')[0]}.csv`;
link.click();
URL.revokeObjectURL(url);
toast.success('Relatório exportado com sucesso!');
} catch (error) {
console.error('Error exporting CSV:', error);
toast.error('Erro ao exportar relatório');
}
};
return {
sessionLogs,
onlineUsers,
loading,
fetchSessionLogs,
fetchOnlineUsers,
exportToCsv
};
};
+84
View File
@@ -0,0 +1,84 @@
import { useEffect, useRef, useCallback } from 'react';
import { useQueryClient } from '@tanstack/react-query';
interface SmartPollingOptions {
queryKey: string[];
interval: number;
enabled?: boolean;
maxRetries?: number;
onError?: (error: Error) => void;
}
export const useSmartPolling = ({
queryKey,
interval,
enabled = true,
maxRetries = 3,
onError
}: SmartPollingOptions) => {
const queryClient = useQueryClient();
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const retryCountRef = useRef(0);
const isActiveRef = useRef(true);
const startPolling = useCallback(() => {
if (!enabled || !isActiveRef.current) return;
intervalRef.current = setInterval(async () => {
try {
if (!document.hidden && isActiveRef.current) {
await queryClient.invalidateQueries({ queryKey });
retryCountRef.current = 0; // Reset retry count on success
}
} catch (error) {
retryCountRef.current++;
if (retryCountRef.current >= maxRetries) {
stopPolling();
onError?.(error as Error);
}
}
}, interval);
}, [queryKey, interval, enabled, maxRetries, onError, queryClient]);
const stopPolling = useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}, []);
// Pausar quando a aba não está ativa
useEffect(() => {
const handleVisibilityChange = () => {
if (document.hidden) {
stopPolling();
} else if (enabled && isActiveRef.current) {
startPolling();
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
}, [startPolling, stopPolling, enabled]);
useEffect(() => {
if (enabled) {
startPolling();
} else {
stopPolling();
}
return stopPolling;
}, [enabled, startPolling, stopPolling]);
useEffect(() => {
return () => {
isActiveRef.current = false;
stopPolling();
};
}, [stopPolling]);
return { startPolling, stopPolling };
};
+656
View File
@@ -0,0 +1,656 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
import { useTasksEnhanced } from '@/hooks/useTasksEnhanced';
import { useUserFunction } from '@/hooks/useUserFunction';
import { useAuth } from '@/hooks/useAuth';
export interface SolicitacaoCompra {
id: string;
numero_sc: string;
data_solicitacao: string;
of_number?: string;
objetivo?: string;
justificativa?: string;
status: string;
revisao: number;
data_previsao_chegada?: string;
created_by?: string;
created_at: string;
updated_at: string;
anexos_urls?: string[];
itens?: ItemSolicitacao[];
creator?: {
id: string;
full_name: string;
email: string;
profile_image_url?: string;
} | null;
}
export interface ItemSolicitacao {
id: string;
solicitacao_id: string;
material_id: string;
quantidade: number;
prazo_recebimento: string;
created_at: string;
material?: {
id: string;
descricao: string;
unidade: string;
tipo_material_id?: string;
};
}
export interface CreateSolicitacaoData {
data_solicitacao: string;
of_number?: string;
objetivo?: string;
justificativa?: string;
anexos_urls?: string[];
itens: Array<{
material_id: string;
quantidade: number;
prazo_recebimento: string;
}>;
}
export const useSolicitacoesCompra = () => {
const queryClient = useQueryClient();
const { createTask, updateTask } = useTasksEnhanced();
const { isComprador } = useUserFunction();
const { user } = useAuth();
const { data: solicitacoes = [], isLoading } = useQuery({
queryKey: ['solicitacoes-compra'],
queryFn: async () => {
// Primeiro buscar as solicitações com seus itens
const { data: solicitacoesData, error: solicitacoesError } = await supabase
.from('solicitacoes_compra')
.select(`
*,
itens:itens_solicitacao_compra(
*,
material:estoque_materiais(id, descricao, unidade, tipo_material_id)
)
`)
.order('created_at', { ascending: false });
if (solicitacoesError) {
console.error('Erro ao buscar solicitações:', solicitacoesError);
throw solicitacoesError;
}
// Se não há solicitações, retornar array vazio
if (!solicitacoesData || solicitacoesData.length === 0) {
return [];
}
// Buscar dados de TODOS os criadores (profiles)
const creatorIds = [...new Set(solicitacoesData.map(s => s.created_by).filter(Boolean))];
if (creatorIds.length === 0) {
return solicitacoesData.map(solicitacao => ({
...solicitacao,
creator: null
}));
}
const { data: creatorsData, error: creatorsError } = await supabase
.from('profiles')
.select('id, full_name, email, profile_image_url')
.in('id', creatorIds);
if (creatorsError) {
console.error('Erro ao buscar criadores:', creatorsError);
// Não falhar completamente se não conseguir buscar os criadores
}
// Combinar os dados
const result: SolicitacaoCompra[] = solicitacoesData.map(solicitacao => ({
...solicitacao,
creator: creatorsData?.find(creator => creator.id === solicitacao.created_by) || {
id: solicitacao.created_by || '',
full_name: 'Usuário não encontrado',
email: '',
profile_image_url: null
}
}));
return result;
},
});
// Função para verificar se pode editar
const canEdit = (solicitacao: SolicitacaoCompra) => {
if (!user?.id) return false;
return user.id === solicitacao.created_by;
};
// Função para verificar se pode excluir
const canDelete = (solicitacao: SolicitacaoCompra) => {
if (!user?.id) return false;
// Comprador pode excluir qualquer solicitação
if (isComprador) {
return true;
}
// Proprietário só pode excluir se estiver "Em planejamento"
if (user.id === solicitacao.created_by && solicitacao.status === 'Em planejamento') {
return true;
}
return false;
};
const createSolicitacao = useMutation({
mutationFn: async (data: CreateSolicitacaoData) => {
// Criar a solicitação com created_by definido automaticamente
const user = await supabase.auth.getUser();
if (!user.data.user) {
throw new Error('Usuário não autenticado');
}
const insertData: any = {
data_solicitacao: data.data_solicitacao,
justificativa: data.justificativa,
created_by: user.data.user.id,
anexos_urls: data.anexos_urls || [],
};
if (data.of_number) {
insertData.of_number = data.of_number;
}
if (data.objetivo) {
insertData.objetivo = data.objetivo;
}
const { data: solicitacao, error: errorSolicitacao } = await supabase
.from('solicitacoes_compra')
.insert(insertData)
.select()
.single();
if (errorSolicitacao) throw errorSolicitacao;
// Criar os itens
const itensData = data.itens.map(item => ({
...item,
solicitacao_id: solicitacao.id,
}));
const { error: errorItens } = await supabase
.from('itens_solicitacao_compra')
.insert(itensData);
if (errorItens) throw errorItens;
return solicitacao;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['solicitacoes-compra'] });
toast.success('Solicitação de compra criada com sucesso!');
},
onError: (error) => {
console.error('Erro ao criar solicitação:', error);
toast.error('Erro ao criar solicitação de compra');
},
});
const updateStatus = useMutation({
mutationFn: async ({ id, status, revisao }: { id: string; status: string; revisao?: number }) => {
const updateData: any = { status };
if (revisao !== undefined) {
updateData.revisao = revisao;
}
const { error } = await supabase
.from('solicitacoes_compra')
.update(updateData)
.eq('id', id);
if (error) throw error;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['solicitacoes-compra'] });
toast.success('Status atualizado com sucesso!');
},
onError: (error) => {
console.error('Erro ao atualizar status:', error);
toast.error('Erro ao atualizar status');
},
});
const revisar = useMutation({
mutationFn: async ({ id, created_by }: { id: string; created_by: string }) => {
console.log('🔄 Iniciando processo de revisão para solicitação:', id);
try {
// Primeiro buscar a solicitação atual
const { data: solicitacaoAtual, error: fetchError } = await supabase
.from('solicitacoes_compra')
.select('revisao, numero_sc')
.eq('id', id)
.maybeSingle();
if (fetchError) {
console.error('❌ Erro ao buscar solicitação:', fetchError);
throw new Error(`Erro ao buscar solicitação: ${fetchError.message}`);
}
if (!solicitacaoAtual) {
console.error('❌ Solicitação não encontrada');
throw new Error('Solicitação não encontrada');
}
console.log('✅ Solicitação encontrada:', solicitacaoAtual);
// Atualizar status para 'Revisado' e incrementar revisão
const { error: updateError } = await supabase
.from('solicitacoes_compra')
.update({
status: 'Revisado',
revisao: (solicitacaoAtual.revisao || 0) + 1
})
.eq('id', id);
if (updateError) {
console.error('❌ Erro ao atualizar status:', updateError);
throw new Error(`Erro ao atualizar status: ${updateError.message}`);
}
console.log('✅ Status atualizado para Revisado');
// Criar tarefa para o usuário que fez a solicitação
const taskData = {
title: `Revisão necessária - SC ${solicitacaoAtual.numero_sc}`,
description: `Sua solicitação de compra ${solicitacaoAtual.numero_sc} precisa de revisão. Clique para visualizar os itens e fazer as correções necessárias.`,
assigned_to: [created_by],
priority: 'media' as const,
category: 'compras' as const,
status: 'a_fazer' as const,
of_number: 'ADM-Compras',
due_date: new Date().toISOString(),
};
console.log('📝 Dados da tarefa a ser criada:', taskData);
await createTask(taskData);
console.log('✅ Tarefa criada com sucesso');
return { numero_sc: solicitacaoAtual.numero_sc };
} catch (error) {
console.error('❌ Erro no processo de revisão:', error);
throw error;
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['solicitacoes-compra'] });
toast.success('Solicitação enviada para revisão e tarefa criada!');
},
onError: (error) => {
console.error('❌ Erro ao revisar solicitação:', error);
toast.error(`Erro ao revisar solicitação: ${error.message}`);
},
});
const aceitar = useMutation({
mutationFn: async ({ id, created_by }: { id: string; created_by: string }) => {
console.log('🔄 Iniciando processo de aceitação para solicitação:', id);
try {
// Primeiro buscar a solicitação atual
const { data: solicitacaoAtual, error: fetchError } = await supabase
.from('solicitacoes_compra')
.select('numero_sc')
.eq('id', id)
.maybeSingle();
if (fetchError) {
console.error('❌ Erro ao buscar solicitação:', fetchError);
throw new Error(`Erro ao buscar solicitação: ${fetchError.message}`);
}
if (!solicitacaoAtual) {
console.error('❌ Solicitação não encontrada');
throw new Error('Solicitação não encontrada');
}
console.log('✅ Solicitação encontrada:', solicitacaoAtual);
// Atualizar status para 'Solicitado'
const { error: updateError } = await supabase
.from('solicitacoes_compra')
.update({ status: 'Solicitado' })
.eq('id', id);
if (updateError) {
console.error('❌ Erro ao atualizar status:', updateError);
throw new Error(`Erro ao atualizar status: ${updateError.message}`);
}
console.log('✅ Status atualizado para Solicitado');
// Criar tarefa informativa para o usuário que fez a solicitação
const taskData = {
title: `SC ${solicitacaoAtual.numero_sc} aceita!`,
description: `Sua solicitação de compra ${solicitacaoAtual.numero_sc} foi aceita pelo comprador e está sendo processada.`,
assigned_to: [created_by],
priority: 'baixa' as const,
category: 'compras' as const,
status: 'a_fazer' as const,
of_number: 'ADM-Compras',
due_date: new Date().toISOString(),
};
console.log('📝 Dados da tarefa a ser criada:', taskData);
await createTask(taskData);
console.log('✅ Tarefa criada com sucesso');
return { numero_sc: solicitacaoAtual.numero_sc };
} catch (error) {
console.error('❌ Erro no processo de aceitação:', error);
throw error;
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['solicitacoes-compra'] });
toast.success('Solicitação aceita e usuário notificado!');
},
onError: (error) => {
console.error('❌ Erro ao aceitar solicitação:', error);
toast.error(`Erro ao aceitar solicitação: ${error.message}`);
},
});
const comprar = useMutation({
mutationFn: async ({ id, created_by }: { id: string; created_by: string }) => {
console.log('🔄 Iniciando processo de compra para solicitação:', id);
try {
// Primeiro buscar a solicitação atual
const { data: solicitacaoAtual, error: fetchError } = await supabase
.from('solicitacoes_compra')
.select('numero_sc')
.eq('id', id)
.maybeSingle();
if (fetchError) {
console.error('❌ Erro ao buscar solicitação:', fetchError);
throw new Error(`Erro ao buscar solicitação: ${fetchError.message}`);
}
if (!solicitacaoAtual) {
console.error('❌ Solicitação não encontrada');
throw new Error('Solicitação não encontrada');
}
console.log('✅ Solicitação encontrada:', solicitacaoAtual);
// Atualizar status para 'Comprado'
const { error: updateError } = await supabase
.from('solicitacoes_compra')
.update({ status: 'Comprado' })
.eq('id', id);
if (updateError) {
console.error('❌ Erro ao atualizar status:', updateError);
throw new Error(`Erro ao atualizar status: ${updateError.message}`);
}
console.log('✅ Status atualizado para Comprado');
// Buscar tarefa relacionada a esta SC
const { data: taskData, error: taskError } = await supabase
.from('tasks')
.select('*')
.ilike('title', `%${solicitacaoAtual.numero_sc}%`)
.contains('assigned_to', [created_by])
.eq('category', 'compras')
.order('created_at', { ascending: false })
.limit(1)
.maybeSingle();
if (taskError) {
console.error('❌ Erro ao buscar tarefa:', taskError);
// Não falhar completamente se não conseguir buscar a tarefa
}
if (taskData) {
console.log('✅ Tarefa encontrada:', taskData.id);
// Atualizar a tarefa existente marcando como concluída com informação de compra
await updateTask({
id: taskData.id,
updates: {
title: `SC ${solicitacaoAtual.numero_sc} - Comprado`,
description: `Sua solicitação de compra ${solicitacaoAtual.numero_sc} foi comprada pelo comprador e está sendo processada.`,
status: 'concluido',
is_completed: true,
completed_at: new Date().toISOString(),
completed_by: user?.id || null,
updated_at: new Date().toISOString()
}
});
console.log('✅ Tarefa atualizada com sucesso');
} else {
console.log('️ Nenhuma tarefa encontrada para atualizar');
}
return { numero_sc: solicitacaoAtual.numero_sc };
} catch (error) {
console.error('❌ Erro no processo de compra:', error);
throw error;
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['solicitacoes-compra'] });
queryClient.invalidateQueries({ queryKey: ['tasks'] });
toast.success('Solicitação marcada como comprada!');
},
onError: (error) => {
console.error('❌ Erro ao marcar como comprada:', error);
toast.error(`Erro ao marcar como comprada: ${error.message}`);
},
});
const comprarDireto = useMutation({
mutationFn: async ({ id, created_by }: { id: string; created_by: string }) => {
console.log('🔄 Iniciando processo de compra direta SIMPLIFICADO para solicitação:', id);
try {
// Buscar dados da solicitação atual
const { data: solicitacaoAtual, error: fetchError } = await supabase
.from('solicitacoes_compra')
.select('numero_sc, status, revisao')
.eq('id', id)
.single();
if (fetchError) {
console.error('❌ Erro ao buscar solicitação:', fetchError);
throw new Error(`Erro ao buscar solicitação: ${fetchError.message}`);
}
console.log('✅ Solicitação encontrada:', solicitacaoAtual);
// Atualização DIRETA e FORÇADA do status
console.log('🔄 Executando atualização FORÇADA do status...');
const { data: updateResult, error: updateError } = await supabase
.from('solicitacoes_compra')
.update({
status: 'Comprado',
revisao: (solicitacaoAtual.revisao || 0) + 1,
updated_at: new Date().toISOString()
})
.eq('id', id)
.select('status, revisao');
if (updateError) {
console.error('❌ Erro na atualização:', updateError);
throw new Error(`Falha na atualização: ${updateError.message}`);
}
console.log('✅ Resultado da atualização:', updateResult);
// Verificação MÚLTIPLA para confirmar se foi atualizado
let verificacaoTentativas = 0;
const maxVerificacoes = 3;
let statusConfirmado = false;
while (verificacaoTentativas < maxVerificacoes && !statusConfirmado) {
verificacaoTentativas++;
// Aguardar antes da verificação
await new Promise(resolve => setTimeout(resolve, 300 * verificacaoTentativas));
const { data: verificacao, error: verError } = await supabase
.from('solicitacoes_compra')
.select('status')
.eq('id', id)
.single();
if (!verError && verificacao?.status === 'Comprado') {
console.log(`✅ Verificação ${verificacaoTentativas}: Status CONFIRMADO como 'Comprado'`);
statusConfirmado = true;
} else {
console.log(`⚠️ Verificação ${verificacaoTentativas}: Status ainda é '${verificacao?.status}' - Tentando novamente...`);
// Forçar nova atualização se necessário
if (verificacaoTentativas < maxVerificacoes) {
await supabase
.from('solicitacoes_compra')
.update({
status: 'Comprado',
updated_at: new Date().toISOString()
})
.eq('id', id);
}
}
}
if (!statusConfirmado) {
throw new Error('FALHA CRÍTICA: Não foi possível confirmar a atualização do status após múltiplas tentativas');
}
// Atualizar tarefa relacionada
try {
const { data: taskData } = await supabase
.from('tasks')
.select('*')
.ilike('title', `%${solicitacaoAtual.numero_sc}%`)
.contains('assigned_to', [created_by])
.eq('category', 'compras')
.order('created_at', { ascending: false })
.limit(1)
.maybeSingle();
if (taskData) {
console.log('✅ Atualizando tarefa relacionada:', taskData.id);
await updateTask({
id: taskData.id,
updates: {
title: `SC ${solicitacaoAtual.numero_sc} - Comprado`,
description: `Sua solicitação de compra ${solicitacaoAtual.numero_sc} foi comprada pelo comprador.`,
status: 'concluido',
is_completed: true,
completed_at: new Date().toISOString(),
completed_by: user?.id || null,
updated_at: new Date().toISOString()
}
});
}
} catch (taskError) {
console.error('⚠️ Erro ao atualizar tarefa (não crítico):', taskError);
}
return {
numero_sc: solicitacaoAtual.numero_sc,
status: 'Comprado',
success: true
};
} catch (error) {
console.error('❌ ERRO CRÍTICO na compra direta:', error);
throw error;
}
},
onSuccess: (data) => {
console.log('✅ SUCESSO TOTAL - Compra direta concluída:', data);
// Invalidar queries IMEDIATAMENTE
queryClient.invalidateQueries({ queryKey: ['solicitacoes-compra'] });
queryClient.invalidateQueries({ queryKey: ['tasks'] });
// Refetch forçado após delays
setTimeout(() => {
queryClient.refetchQueries({ queryKey: ['solicitacoes-compra'] });
}, 200);
setTimeout(() => {
queryClient.refetchQueries({ queryKey: ['solicitacoes-compra'] });
}, 1000);
setTimeout(() => {
queryClient.refetchQueries({ queryKey: ['solicitacoes-compra'] });
}, 1000);
toast.success(`✅ SC ${data.numero_sc} marcada como COMPRADA com sucesso!`);
},
onError: (error) => {
console.error('❌ ERRO FINAL na compra direta:', error);
toast.error(`❌ Falha ao marcar como comprada: ${error.message}`);
// Refetch mesmo com erro para verificar estado atual
setTimeout(() => {
queryClient.refetchQueries({ queryKey: ['solicitacoes-compra'] });
}, 1000);
},
});
const deleteSolicitacao = useMutation({
mutationFn: async (id: string) => {
const { error } = await supabase
.from('solicitacoes_compra')
.delete()
.eq('id', id);
if (error) throw error;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['solicitacoes-compra'] });
toast.success('Solicitação excluída com sucesso!');
},
onError: (error) => {
console.error('Erro ao excluir solicitação:', error);
toast.error('Erro ao excluir solicitação');
},
});
return {
solicitacoes,
isLoading,
canEdit,
canDelete,
createSolicitacao: createSolicitacao.mutate,
updateStatus: updateStatus.mutate,
revisar: revisar.mutate,
aceitar: aceitar.mutate,
comprar: comprar.mutate,
comprarDireto: comprarDireto.mutate,
deleteSolicitacao: deleteSolicitacao.mutate,
isCreating: createSolicitacao.isPending,
isUpdating: updateStatus.isPending,
isRevisando: revisar.isPending,
isAceitando: aceitar.isPending,
isComprando: comprar.isPending,
isComprandoDireto: comprarDireto.isPending,
isDeleting: deleteSolicitacao.isPending,
};
};
+179
View File
@@ -0,0 +1,179 @@
import { useState, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
import { useAuth } from '@/hooks/useAuth';
export interface Sugestao {
id: string;
created_at: string;
sugestao: string;
user_id: string;
user_name: string;
status: 'Pendente' | 'Implementada' | 'Rejeitada';
developer_notes: string | null;
archived_at: string | null;
archived_by: string | null;
}
export interface SugestaoNotification {
id: string;
user_id: string;
sugestao_id: string;
message: string;
is_read: boolean;
created_at: string;
}
export const useSugestoes = () => {
const { user } = useAuth();
const queryClient = useQueryClient();
// Buscar sugestões não arquivadas
const { data: sugestoes = [], isLoading, error } = useQuery({
queryKey: ['sugestoes'],
queryFn: async () => {
const { data, error } = await supabase
.from('sugestoes')
.select('*')
.is('archived_at', null)
.order('created_at', { ascending: false });
if (error) throw error;
return data as Sugestao[];
},
});
// Buscar sugestões arquivadas
const { data: sugestoesArquivadas = [], isLoading: isLoadingArchived } = useQuery({
queryKey: ['sugestoes-arquivadas'],
queryFn: async () => {
const { data, error } = await supabase
.from('sugestoes')
.select('*')
.not('archived_at', 'is', null)
.order('archived_at', { ascending: false });
if (error) throw error;
return data as Sugestao[];
},
});
// Buscar notificações do usuário
const { data: notifications = [] } = useQuery({
queryKey: ['sugestao-notifications'],
queryFn: async () => {
if (!user) return [];
const { data, error } = await supabase
.from('sugestao_notifications')
.select('*')
.eq('user_id', user.id)
.eq('is_read', false)
.order('created_at', { ascending: false });
if (error) throw error;
return data as SugestaoNotification[];
},
enabled: !!user,
});
// Mutation para criar nova sugestão
const createSugestaeMutation = useMutation({
mutationFn: async (sugestao: string) => {
if (!user) throw new Error('Usuário não autenticado');
const { data, error } = await supabase
.from('sugestoes')
.insert({
sugestao,
user_id: user.id,
user_name: user.user_metadata?.full_name || user.email || 'Usuário'
})
.select()
.single();
if (error) throw error;
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['sugestoes'] });
toast.success('Sugestão enviada com sucesso!');
},
onError: (error) => {
console.error('Erro ao enviar sugestão:', error);
toast.error('Erro ao enviar sugestão');
},
});
// Mutation para atualizar status da sugestão (admin only)
const updateSugestaoMutation = useMutation({
mutationFn: async ({ id, status, developer_notes }: {
id: string;
status: string;
developer_notes?: string;
}) => {
const { data, error } = await supabase
.from('sugestoes')
.update({ status, developer_notes })
.eq('id', id)
.select()
.single();
if (error) throw error;
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['sugestoes'] });
queryClient.invalidateQueries({ queryKey: ['sugestoes-arquivadas'] });
queryClient.invalidateQueries({ queryKey: ['sugestao-notifications'] });
toast.success('Sugestão atualizada com sucesso!');
},
onError: (error) => {
console.error('Erro ao atualizar sugestão:', error);
toast.error('Erro ao atualizar sugestão');
},
});
// Mutation para marcar notificação como lida
const markNotificationAsReadMutation = useMutation({
mutationFn: async (notificationId: string) => {
const { error } = await supabase
.from('sugestao_notifications')
.update({ is_read: true })
.eq('id', notificationId);
if (error) throw error;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['sugestao-notifications'] });
},
});
const createSugestao = (sugestao: string) => {
createSugestaeMutation.mutate(sugestao);
};
const updateSugestao = (id: string, status: string, developer_notes?: string) => {
updateSugestaoMutation.mutate({ id, status, developer_notes });
};
const markNotificationAsRead = (notificationId: string) => {
markNotificationAsReadMutation.mutate(notificationId);
};
return {
sugestoes,
sugestoesArquivadas,
notifications,
loading: isLoading,
loadingArchived: isLoadingArchived,
error,
createSugestao,
updateSugestao,
markNotificationAsRead,
isCreating: createSugestaeMutation.isPending,
isUpdating: updateSugestaoMutation.isPending,
};
};
+168
View File
@@ -0,0 +1,168 @@
import { useState, useEffect } from 'react';
import { useUserPermissions } from '@/hooks/useUserPermissions';
export interface SystemMapNode {
id: string;
left: number;
top: number;
title: string;
description: string;
icon: string;
color: string;
group: string;
profiles: string[];
hasAccess: boolean;
isMainFlow?: boolean;
url?: string;
}
export interface SystemMapConnection {
id: string;
from: string;
to: string;
color?: string;
type?: string;
}
export function useSystemMapAutoDiscovery() {
const [nodes, setNodes] = useState<SystemMapNode[]>([]);
const [connections, setConnections] = useState<SystemMapConnection[]>([]);
const [loading, setLoading] = useState(false);
const { hasAccess, loading: permissionsLoading } = useUserPermissions();
const generateSystemMap = async () => {
if (permissionsLoading) return;
setLoading(true);
try {
// Auto-discover system components based on user permissions
const discoveredNodes: SystemMapNode[] = [];
const discoveredConnections: SystemMapConnection[] = [];
// Check access to different routes and create nodes accordingly
const routes = [
{
path: '/equipamentos',
title: 'Equipamentos',
description: 'Gestão de equipamentos e ferramentas',
icon: '🔧',
color: '#3b82f6',
group: 'Cadastros'
},
{
path: '/dashboard',
title: 'Dashboard',
description: 'Painel principal do sistema',
icon: '📊',
color: '#10b981',
group: 'Principal'
},
{
path: '/producao',
title: 'Produção',
description: 'Controle de produção',
icon: '🏭',
color: '#f59e0b',
group: 'Principal'
},
{
path: '/estoque',
title: 'Estoque',
description: 'Gestão de materiais',
icon: '📦',
color: '#8b5cf6',
group: 'Principal'
},
{
path: '/ofs',
title: 'Ordens de Fabricação',
description: 'Gestão de OFs',
icon: '📋',
color: '#ef4444',
group: 'Principal'
},
{
path: '/expedicao',
title: 'Expedição',
description: 'Controle de expedição',
icon: '🚛',
color: '#06b6d4',
group: 'Principal'
},
{
path: '/admin',
title: 'Administração',
description: 'Configurações do sistema',
icon: '⚙️',
color: '#6b7280',
group: 'Admin'
}
];
routes.forEach((route, index) => {
// Use hasAccess with resource mapping instead of canAccessRoute
let resourceKey = '';
switch (route.path) {
case '/equipamentos':
resourceKey = 'equipamentos';
break;
case '/admin':
resourceKey = 'admin';
break;
default:
resourceKey = '';
}
const canAccess = resourceKey ? hasAccess(resourceKey) : hasAccess();
discoveredNodes.push({
id: `node-${index}`,
left: (index % 3) * 250 + 50,
top: Math.floor(index / 3) * 150 + 50,
title: route.title,
description: route.description,
icon: route.icon,
color: route.color,
group: route.group,
profiles: ['Usuário', 'Admin'],
hasAccess: canAccess,
isMainFlow: route.group === 'Principal',
url: route.path
});
});
// Create connections between related nodes
for (let i = 0; i < discoveredNodes.length - 1; i++) {
discoveredConnections.push({
id: `edge-${i}`,
from: discoveredNodes[i].id,
to: discoveredNodes[i + 1].id,
color: '#9ca3af',
type: 'smoothstep'
});
}
setNodes(discoveredNodes);
setConnections(discoveredConnections);
} catch (error) {
console.error('Error generating system map:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (!permissionsLoading) {
generateSystemMap();
}
}, [permissionsLoading, hasAccess]);
return {
nodes,
connections,
loading,
regenerateMap: generateSystemMap
};
}
+93
View File
@@ -0,0 +1,93 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
import { useAuth } from '@/hooks/useAuth';
export const useTaskActions = () => {
const queryClient = useQueryClient();
const { user } = useAuth();
const { mutate: updateTaskStatus, isPending: isUpdatingStatus } = useMutation({
mutationFn: async ({ id, status }: { id: string; status: string }) => {
const updates: any = {
status,
updated_at: new Date().toISOString()
};
if (status === 'concluido') {
updates.is_completed = true;
updates.completed_at = new Date().toISOString();
updates.completed_by = user?.id;
} else if (status === 'em_andamento') {
updates.is_completed = false;
updates.completed_at = null;
updates.completed_by = null;
}
const { data, error } = await supabase
.from('tasks')
.update(updates)
.eq('id', id)
.select()
.single();
if (error) throw error;
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] });
toast.success('Status da tarefa atualizado com sucesso!');
},
onError: (error) => {
console.error('Error updating task status:', error);
toast.error('Erro ao atualizar status da tarefa');
},
});
const { mutate: deleteTask, isPending: isDeleting } = useMutation({
mutationFn: async (id: string) => {
const { error } = await supabase
.from('tasks')
.delete()
.eq('id', id);
if (error) throw error;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] });
toast.success('Tarefa deletada com sucesso!');
},
onError: (error) => {
console.error('Error deleting task:', error);
toast.error('Erro ao deletar tarefa');
},
});
const canEdit = (task: any) => {
return user?.id === task.created_by;
};
const canDelete = (task: any) => {
return user?.id === task.created_by;
};
const canMarkComplete = (task: any) => {
return task.assigned_to?.includes(user?.id) || user?.id === task.created_by;
};
const canAccept = (task: any) => {
return task.assigned_to?.includes(user?.id) && task.status === 'a_fazer';
};
return {
updateTaskStatus,
deleteTask,
canEdit,
canDelete,
canMarkComplete,
canAccept,
isUpdatingStatus,
isDeleting,
};
};
+476
View File
@@ -0,0 +1,476 @@
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { Database } from '@/integrations/supabase/types';
import { toast } from 'sonner';
type Task = Database['public']['Tables']['tasks']['Row'];
type TaskInsert = Database['public']['Tables']['tasks']['Insert'];
type TaskUpdate = Database['public']['Tables']['tasks']['Update'];
export const useTasks = () => {
const queryClient = useQueryClient();
// Fetch active tasks (not completed)
const {
data: activeTasks = [],
isLoading: isLoadingActive,
refetch: refetchActiveTasks,
error: activeTasksError
} = useQuery({
queryKey: ['tasks', 'active'],
queryFn: async () => {
console.log('🔍 Fetching active tasks...');
// Check if user is authenticated
const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError) {
console.error('❌ Auth error when fetching active tasks:', authError);
throw new Error('Erro de autenticação: ' + authError.message);
}
if (!user) {
console.error('❌ No authenticated user found');
throw new Error('Usuário não autenticado');
}
console.log('✅ User authenticated:', user.email);
const { data, error } = await supabase
.from('tasks')
.select('*')
.eq('is_completed', false)
.order('due_date', { ascending: true, nullsFirst: false });
if (error) {
console.error('❌ Error fetching active tasks:', error);
console.error('Error details:', {
code: error.code,
message: error.message,
details: error.details,
hint: error.hint
});
throw new Error('Erro ao buscar tarefas ativas: ' + error.message);
}
console.log('✅ Active tasks fetched successfully:', data?.length || 0, 'tasks');
console.log('📋 Tasks data:', data);
return data || [];
},
});
// Fetch recent completed tasks (last 24 hours)
const {
data: recentCompletedTasks = [],
isLoading: isLoadingRecentCompleted,
refetch: refetchRecentCompleted,
error: recentCompletedError
} = useQuery({
queryKey: ['tasks', 'recent-completed'],
queryFn: async () => {
console.log('🔍 Fetching recent completed tasks...');
// Check if user is authenticated
const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError || !user) {
console.error('❌ Auth error when fetching recent completed tasks:', authError);
throw new Error('Usuário não autenticado');
}
const oneDayAgo = new Date();
oneDayAgo.setDate(oneDayAgo.getDate() - 1);
const { data, error } = await supabase
.from('tasks')
.select('*')
.eq('is_completed', true)
.gte('completed_at', oneDayAgo.toISOString())
.order('completed_at', { ascending: false });
if (error) {
console.error('❌ Error fetching recent completed tasks:', error);
throw new Error('Erro ao buscar tarefas concluídas: ' + error.message);
}
console.log('✅ Recent completed tasks fetched:', data?.length || 0);
return data || [];
},
});
// Fetch completed tasks by current user (last 30 days for history)
const {
data: completedTasks = [],
isLoading: isLoadingCompleted,
refetch: refetchCompletedTasks,
error: completedTasksError
} = useQuery({
queryKey: ['tasks', 'completed'],
queryFn: async () => {
console.log('🔍 Fetching completed tasks by current user...');
// Check if user is authenticated
const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError || !user) {
console.error('❌ Auth error when fetching completed tasks:', authError);
throw new Error('Usuário não autenticado');
}
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const { data, error } = await supabase
.from('tasks')
.select('*')
.eq('is_completed', true)
.eq('completed_by', user.id)
.gte('completed_at', thirtyDaysAgo.toISOString())
.order('completed_at', { ascending: false });
if (error) {
console.error('❌ Error fetching completed tasks:', error);
throw new Error('Erro ao buscar histórico de tarefas: ' + error.message);
}
console.log('✅ Completed tasks by user fetched:', data?.length || 0);
return data || [];
},
});
// Fetch tasks assigned by current user and completed by others (last 30 days for history)
const {
data: assignedCompletedTasks = [],
isLoading: isLoadingAssignedCompleted,
refetch: refetchAssignedCompletedTasks,
error: assignedCompletedTasksError
} = useQuery({
queryKey: ['tasks', 'assigned-completed'],
queryFn: async () => {
console.log('🔍 Fetching assigned completed tasks...');
// Check if user is authenticated
const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError || !user) {
console.error('❌ Auth error when fetching assigned completed tasks:', authError);
throw new Error('Usuário não autenticado');
}
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const { data, error } = await supabase
.from('tasks')
.select('*')
.eq('is_completed', true)
.eq('created_by', user.id)
.neq('completed_by', user.id)
.gte('completed_at', thirtyDaysAgo.toISOString())
.order('completed_at', { ascending: false });
if (error) {
console.error('❌ Error fetching assigned completed tasks:', error);
throw new Error('Erro ao buscar tarefas atribuídas concluídas: ' + error.message);
}
console.log('✅ Assigned completed tasks fetched:', data?.length || 0);
return data || [];
},
});
// Fetch available OFs from ficha_tecnica_contratos table
const { data: availableOFs = [] } = useQuery({
queryKey: ['ofs'],
queryFn: async () => {
console.log('🔍 Fetching available OFs...');
const { data, error } = await supabase
.from('ficha_tecnica_contratos')
.select('of_number, cliente')
.order('of_number');
if (error) {
console.error('❌ Error fetching OFs:', error);
throw error;
}
console.log('✅ Available OFs fetched:', data?.length || 0);
return data || [];
},
});
// Fetch available users - use direct query with type assertion for RPC
const { data: availableUsers = [] } = useQuery({
queryKey: ['users'],
queryFn: async () => {
console.log('🔍 Fetching available users...');
// Check if user is authenticated
const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError || !user) {
console.error('❌ Auth error when fetching users:', authError);
throw new Error('Usuário não autenticado');
}
console.log('✅ User authenticated for fetching users:', user.email);
try {
// Use RPC function to bypass RLS restrictions for user listing
const { data, error } = await supabase.rpc('get_all_users_for_tasks' as any);
if (error) {
console.error('❌ Error fetching users via RPC:', error);
// Fallback to direct query if RPC fails
const { data: fallbackData, error: fallbackError } = await supabase
.from('profiles')
.select('id, email, full_name')
.eq('status', 'active')
.order('email');
if (fallbackError) {
console.error('❌ Error fetching users (fallback):', fallbackError);
throw new Error('Erro ao buscar usuários: ' + fallbackError.message);
}
console.log('✅ Available users fetched (fallback):', fallbackData?.length || 0);
return fallbackData || [];
}
// Type assertion for RPC result
const typedData = data as Array<{ id: string; email: string; full_name: string }>;
console.log('✅ Available users fetched via RPC:', typedData?.length || 0);
return typedData || [];
} catch (rpcError) {
console.error('❌ RPC call failed, using fallback:', rpcError);
// Fallback to direct query
const { data: fallbackData, error: fallbackError } = await supabase
.from('profiles')
.select('id, email, full_name')
.eq('status', 'active')
.order('email');
if (fallbackError) {
console.error('❌ Error fetching users (fallback):', fallbackError);
throw new Error('Erro ao buscar usuários: ' + fallbackError.message);
}
console.log('✅ Available users fetched (fallback):', fallbackData?.length || 0);
return fallbackData || [];
}
},
});
// Create task mutation
const { mutate: createTask, isPending: isCreating } = useMutation({
mutationFn: async (taskData: Omit<TaskInsert, 'task_ref' | 'id' | 'created_at' | 'updated_at' | 'created_by'>) => {
console.log('🚀 Creating task with data:', taskData);
// Check if user is authenticated
const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError) {
console.error('❌ Auth error when creating task:', authError);
throw new Error('Erro de autenticação: ' + authError.message);
}
if (!user) {
console.error('❌ No authenticated user found when creating task');
throw new Error('Usuário não autenticado');
}
console.log('✅ User authenticated for task creation:', user.email);
// Generate a unique task_ref using timestamp and random string
const timestamp = Date.now();
const randomString = Math.random().toString(36).substring(2, 8);
const uniqueTaskRef = `TASK-${timestamp}-${randomString}`;
// Prepare the complete task data
const completeTaskData: TaskInsert = {
...taskData,
created_by: user.id,
task_ref: uniqueTaskRef, // Use unique task_ref instead of placeholder
};
console.log('📝 Complete task data for insertion:', completeTaskData);
const { data, error } = await supabase
.from('tasks')
.insert(completeTaskData)
.select()
.single();
if (error) {
console.error('❌ Error creating task:', error);
console.error('Error details:', {
code: error.code,
message: error.message,
details: error.details,
hint: error.hint
});
throw new Error('Erro ao criar tarefa: ' + error.message);
}
console.log('✅ Task created successfully:', data);
return data;
},
onSuccess: (data) => {
console.log('🎉 Task creation successful, invalidating queries...');
queryClient.invalidateQueries({ queryKey: ['tasks'] });
toast.success('Tarefa criada com sucesso!');
},
onError: (error) => {
console.error('💥 Task creation failed:', error);
const errorMessage = error instanceof Error ? error.message : 'Erro desconhecido ao criar tarefa';
toast.error(errorMessage);
},
});
// Update task mutation
const { mutate: updateTask, isPending: isUpdating } = useMutation({
mutationFn: async ({ id, updates }: { id: string; updates: TaskUpdate }) => {
console.log('📝 Updating task:', id, updates);
const { data, error } = await supabase
.from('tasks')
.update(updates)
.eq('id', id)
.select()
.single();
if (error) {
console.error('❌ Error updating task:', error);
throw new Error('Erro ao atualizar tarefa: ' + error.message);
}
console.log('✅ Task updated successfully:', data);
return data;
},
onSuccess: () => {
console.log('🎉 Task update successful, invalidating queries...');
queryClient.invalidateQueries({ queryKey: ['tasks'] });
toast.success('Tarefa atualizada com sucesso!');
},
onError: (error) => {
console.error('💥 Task update failed:', error);
const errorMessage = error instanceof Error ? error.message : 'Erro ao atualizar tarefa';
toast.error(errorMessage);
},
});
// Complete task mutation
const { mutate: completeTask, isPending: isCompleting } = useMutation({
mutationFn: async (id: string) => {
console.log('✅ Completing task:', id);
const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error('User not authenticated');
const { data, error } = await supabase
.from('tasks')
.update({
is_completed: true,
completed_at: new Date().toISOString(),
completed_by: user.id,
status: 'concluido'
})
.eq('id', id)
.select()
.single();
if (error) {
console.error('❌ Error completing task:', error);
throw new Error('Erro ao concluir tarefa: ' + error.message);
}
console.log('✅ Task completed successfully:', data);
return data;
},
onSuccess: () => {
console.log('🎉 Task completion successful, invalidating queries...');
queryClient.invalidateQueries({ queryKey: ['tasks'] });
toast.success('Tarefa concluída com sucesso!');
},
onError: (error) => {
console.error('💥 Task completion failed:', error);
const errorMessage = error instanceof Error ? error.message : 'Erro ao concluir tarefa';
toast.error(errorMessage);
},
});
// Delete task mutation
const { mutate: deleteTask, isPending: isDeleting } = useMutation({
mutationFn: async (id: string) => {
console.log('🗑️ Deleting task:', id);
const { error } = await supabase
.from('tasks')
.delete()
.eq('id', id);
if (error) {
console.error('❌ Error deleting task:', error);
throw new Error('Erro ao deletar tarefa: ' + error.message);
}
console.log('✅ Task deleted successfully');
},
onSuccess: () => {
console.log('🎉 Task deletion successful, invalidating queries...');
queryClient.invalidateQueries({ queryKey: ['tasks'] });
toast.success('Tarefa deletada com sucesso!');
},
onError: (error) => {
console.error('💥 Task deletion failed:', error);
const errorMessage = error instanceof Error ? error.message : 'Erro ao deletar tarefa';
toast.error(errorMessage);
},
});
// Refetch all tasks function
const refetchTasks = () => {
console.log('🔄 Refetching all tasks...');
refetchActiveTasks();
refetchRecentCompleted();
refetchCompletedTasks();
refetchAssignedCompletedTasks();
};
const isLoading = isLoadingActive || isLoadingRecentCompleted || isLoadingCompleted || isLoadingAssignedCompleted;
// Log any query errors
if (activeTasksError) {
console.error('❌ Active tasks query error:', activeTasksError);
}
if (recentCompletedError) {
console.error('❌ Recent completed tasks query error:', recentCompletedError);
}
if (completedTasksError) {
console.error('❌ Completed tasks query error:', completedTasksError);
}
if (assignedCompletedTasksError) {
console.error('❌ Assigned completed tasks query error:', assignedCompletedTasksError);
}
return {
activeTasks,
recentCompletedTasks,
completedTasks,
assignedCompletedTasks,
availableOFs,
availableUsers,
isLoading,
createTask,
updateTask,
deleteTask,
completeTask,
isCreating,
isUpdating,
isDeleting,
isCompleting,
refetchTasks,
// Expose errors for debugging
errors: {
activeTasksError,
recentCompletedError,
completedTasksError,
assignedCompletedTasksError
}
};
};
+548
View File
@@ -0,0 +1,548 @@
import { useState, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { Task, TaskInsert, TaskUpdate, TaskFilters, TaskCounters } from '@/types/tasks';
import { toast } from 'sonner';
import { useAuth } from '@/hooks/useAuth';
export const useTasksEnhanced = () => {
const queryClient = useQueryClient();
const { user } = useAuth();
// Helper function to apply filters to a query
const applyFilters = (query: any, filters: TaskFilters) => {
if (filters.of_number) {
query = query.eq('of_number', filters.of_number);
}
if (filters.priority) {
query = query.eq('priority', filters.priority);
}
if (filters.status) {
query = query.eq('status', filters.status);
}
if (filters.assigned_to) {
query = query.contains('assigned_to', [filters.assigned_to]);
}
if (filters.task_ref) {
query = query.ilike('task_ref', `%${filters.task_ref}%`);
}
if (filters.search) {
query = query.or(`title.ilike.%${filters.search}%,description.ilike.%${filters.search}%,task_ref.ilike.%${filters.search}%`);
}
if (filters.due_date_range) {
const now = new Date();
switch (filters.due_date_range) {
case 'week':
const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
query = query.gte('due_date', now.toISOString()).lte('due_date', nextWeek.toISOString());
break;
case 'month':
const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, now.getDate());
query = query.gte('due_date', now.toISOString()).lte('due_date', nextMonth.toISOString());
break;
case 'overdue':
query = query.lt('due_date', now.toISOString());
break;
}
}
return query;
};
// Fetch "Minhas Tarefas" (tasks assigned to current user, not completed)
const fetchMyTasks = async (filters: TaskFilters = {}) => {
if (!user) throw new Error('User not authenticated');
console.log('🔍 Fetching my tasks with filters:', filters);
let query = supabase
.from('tasks')
.select(`
*,
task_comments(count),
task_attachments(count),
task_subtasks(count)
`)
.contains('assigned_to', [user.id])
.eq('is_completed', false)
.is('archived_at', null);
query = applyFilters(query, filters);
const { data: tasks, error } = await query.order('due_date', { ascending: true, nullsFirst: false });
if (error) {
console.error('❌ Error fetching my tasks:', error);
throw error;
}
console.log('✅ My tasks fetched:', tasks?.length || 0, 'tasks');
// Fetch creator profiles separately to avoid join issues
const creatorIds = [...new Set(tasks?.map(task => task.created_by).filter(Boolean) || [])];
if (creatorIds.length > 0) {
const { data: profiles } = await supabase
.from('profiles')
.select('id, full_name, email, profile_image_url')
.in('id', creatorIds);
// Combine tasks with creator profiles
const tasksWithProfiles = tasks?.map(task => ({
...task,
creator_profile: profiles?.find(profile => profile.id === task.created_by) || null
})) || [];
return tasksWithProfiles;
}
return tasks?.map(task => ({
...task,
creator_profile: null
})) || [];
};
// Fetch "Minhas Tarefas Concluídas" (completed tasks assigned to current user)
const fetchMyCompletedTasks = async (filters: TaskFilters = {}) => {
if (!user) throw new Error('User not authenticated');
console.log('🔍 Fetching my completed tasks with filters:', filters);
let query = supabase
.from('tasks')
.select(`
*,
task_comments(count),
task_attachments(count),
task_subtasks(count)
`)
.contains('assigned_to', [user.id])
.eq('is_completed', true)
.is('archived_at', null);
query = applyFilters(query, filters);
const { data: tasks, error } = await query.order('completed_at', { ascending: false });
if (error) {
console.error('❌ Error fetching my completed tasks:', error);
throw error;
}
console.log('✅ My completed tasks fetched:', tasks?.length || 0, 'tasks');
// Fetch creator profiles separately
const creatorIds = [...new Set(tasks?.map(task => task.created_by).filter(Boolean) || [])];
if (creatorIds.length > 0) {
const { data: profiles } = await supabase
.from('profiles')
.select('id, full_name, email, profile_image_url')
.in('id', creatorIds);
// Combine tasks with creator profiles
const tasksWithProfiles = tasks?.map(task => ({
...task,
creator_profile: profiles?.find(profile => profile.id === task.created_by) || null
})) || [];
return tasksWithProfiles;
}
return tasks?.map(task => ({
...task,
creator_profile: null
})) || [];
};
// Fetch "Tarefas Atribuídas" (tasks created by current user and assigned to others)
const fetchAssignedTasks = async (filters: TaskFilters = {}) => {
if (!user) throw new Error('User not authenticated');
console.log('🔍 Fetching assigned tasks with filters:', filters);
let query = supabase
.from('tasks')
.select(`
*,
task_comments(count),
task_attachments(count),
task_subtasks(count)
`)
.eq('created_by', user.id)
.is('archived_at', null);
query = applyFilters(query, filters);
const { data: tasks, error } = await query.order('created_at', { ascending: false });
if (error) {
console.error('❌ Error fetching assigned tasks:', error);
throw error;
}
console.log('✅ Assigned tasks fetched:', tasks?.length || 0, 'tasks');
// For assigned tasks, fetch assigned user profiles
const assignedUserIds = [...new Set(tasks?.flatMap(task => task.assigned_to || []).filter(Boolean) || [])];
if (assignedUserIds.length > 0) {
const { data: profiles } = await supabase
.from('profiles')
.select('id, full_name, email, profile_image_url')
.in('id', assignedUserIds);
// Combine tasks with creator and assigned profiles
const tasksWithProfiles = tasks?.map(task => ({
...task,
creator_profile: {
id: user.id,
full_name: user.user_metadata?.full_name || null,
email: user.email || null,
profile_image_url: null
},
assigned_profiles: task.assigned_to?.map(userId =>
profiles?.find(profile => profile.id === userId) || null
).filter(Boolean) || []
})) || [];
return tasksWithProfiles;
}
return tasks?.map(task => ({
...task,
creator_profile: {
id: user.id,
full_name: user.user_metadata?.full_name || null,
email: user.email || null,
profile_image_url: null
},
assigned_profiles: []
})) || [];
};
// Fetch task counters
const fetchTaskCounters = async (): Promise<TaskCounters> => {
if (!user) throw new Error('User not authenticated');
const [myTasksResult, assignedTasksResult] = await Promise.all([
supabase
.from('tasks')
.select('id', { count: 'exact' })
.contains('assigned_to', [user.id])
.eq('is_completed', false),
supabase
.from('tasks')
.select('id', { count: 'exact' })
.eq('created_by', user.id)
.eq('is_completed', false)
]);
if (myTasksResult.error) throw myTasksResult.error;
if (assignedTasksResult.error) throw assignedTasksResult.error;
return {
myPendingTasks: myTasksResult.count || 0,
tasksIAssigned: assignedTasksResult.count || 0,
};
};
// Fetch available users - use type assertion for RPC function
const { data: availableUsers = [] } = useQuery({
queryKey: ['users-for-tasks'],
queryFn: async () => {
console.log('🔍 Fetching available users for tasks...');
if (!user) {
console.error('❌ No authenticated user');
throw new Error('Usuário não autenticado');
}
console.log('✅ User authenticated for fetching users:', user.email);
try {
// Use RPC function to bypass RLS restrictions
const { data, error } = await supabase.rpc('get_all_users_for_tasks');
if (error) {
console.error('❌ Error fetching users via RPC:', error);
// Fallback to direct query
const { data: fallbackData, error: fallbackError } = await supabase
.from('profiles')
.select('id, email, full_name')
.eq('status', 'active')
.order('full_name', { nullsFirst: false })
.order('email');
if (fallbackError) {
console.error('❌ Error fetching users (fallback):', fallbackError);
throw new Error('Erro ao buscar usuários: ' + fallbackError.message);
}
console.log('✅ Available users fetched (fallback):', fallbackData?.length || 0);
return fallbackData || [];
}
console.log('✅ Available users fetched via RPC:', data?.length || 0);
return data || [];
} catch (rpcError) {
console.error('❌ RPC call failed, using fallback:', rpcError);
// Fallback to direct query
const { data: fallbackData, error: fallbackError } = await supabase
.from('profiles')
.select('id, email, full_name')
.eq('status', 'active')
.order('full_name', { nullsFirst: false })
.order('email');
if (fallbackError) {
console.error('❌ Error fetching users (fallback):', fallbackError);
throw new Error('Erro ao buscar usuários: ' + fallbackError.message);
}
console.log('✅ Available users fetched (fallback):', fallbackData?.length || 0);
return fallbackData || [];
}
},
enabled: !!user,
});
// Fetch available OFs
const { data: availableOFs = [] } = useQuery({
queryKey: ['ofs-for-tasks'],
queryFn: async () => {
console.log('🔍 Fetching available OFs for tasks...');
if (!user) throw new Error('Usuário não autenticado');
const { data, error } = await supabase
.from('ficha_tecnica_contratos')
.select('of_number, cliente')
.order('of_number');
if (error) {
console.error('❌ Error fetching OFs:', error);
throw error;
}
console.log('✅ Available OFs fetched:', data?.length || 0);
return data || [];
},
enabled: !!user,
});
// Accept task mutation
const { mutate: acceptTask, isPending: isAccepting } = useMutation({
mutationFn: async (taskId: string) => {
if (!user) throw new Error('User not authenticated');
console.log('✅ Accepting task:', taskId);
const { data, error } = await supabase
.from('tasks')
.update({
status: 'em_andamento',
updated_at: new Date().toISOString()
})
.eq('id', taskId)
.select()
.single();
if (error) {
console.error('❌ Error accepting task:', error);
throw error;
}
console.log('✅ Task accepted successfully:', data);
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] });
queryClient.invalidateQueries({ queryKey: ['task-counters'] });
toast.success('Tarefa aceita com sucesso!');
},
onError: (error) => {
console.error('❌ Error accepting task:', error);
toast.error('Erro ao aceitar tarefa');
},
});
// Create task mutation
const { mutate: createTask, isPending: isCreating } = useMutation({
mutationFn: async (taskData: Omit<TaskInsert, 'task_ref' | 'id' | 'created_at' | 'updated_at' | 'created_by'>) => {
if (!user) throw new Error('User not authenticated');
console.log('📝 Creating task with data:', taskData);
// Generate a unique task_ref using timestamp and random string
const timestamp = Date.now();
const randomString = Math.random().toString(36).substring(2, 8);
const uniqueTaskRef = `TASK-${timestamp}-${randomString}`;
const completeTaskData: TaskInsert = {
...taskData,
created_by: user.id,
task_ref: uniqueTaskRef, // Use unique task_ref instead of placeholder
};
console.log('📤 Sending complete task data to database:', completeTaskData);
const { data, error } = await supabase
.from('tasks')
.insert(completeTaskData)
.select()
.single();
if (error) {
console.error('❌ Error creating task in database:', error);
throw error;
}
console.log('✅ Task created successfully:', data);
return data;
},
onSuccess: (data) => {
console.log('🎉 Task creation successful, invalidating queries...');
queryClient.invalidateQueries({ queryKey: ['tasks'] });
queryClient.invalidateQueries({ queryKey: ['task-counters'] });
toast.success(`Tarefa ${data.task_ref} criada com sucesso!`);
// Force refresh of assigned tasks specifically
setTimeout(() => {
console.log('🔄 Force refreshing assigned tasks...');
queryClient.invalidateQueries({ queryKey: ['tasks', 'assigned_tasks'] });
}, 500);
},
onError: (error) => {
console.error('❌ Error creating task:', error);
toast.error('Erro ao criar tarefa. Verifique os dados e tente novamente.');
},
});
// Update task mutation
const { mutate: updateTask, isPending: isUpdating } = useMutation({
mutationFn: async ({ id, updates }: { id: string; updates: TaskUpdate }) => {
const { data, error } = await supabase
.from('tasks')
.update(updates)
.eq('id', id)
.select()
.single();
if (error) throw error;
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] });
queryClient.invalidateQueries({ queryKey: ['task-counters'] });
toast.success('Tarefa atualizada com sucesso!');
},
onError: (error) => {
console.error('Error updating task:', error);
toast.error('Erro ao atualizar tarefa');
},
});
// Complete task mutation
const { mutate: completeTask, isPending: isCompleting } = useMutation({
mutationFn: async (id: string) => {
if (!user) throw new Error('User not authenticated');
const { data, error } = await supabase
.from('tasks')
.update({
is_completed: true,
completed_at: new Date().toISOString(),
completed_by: user.id,
status: 'concluido'
})
.eq('id', id)
.select()
.single();
if (error) throw error;
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] });
queryClient.invalidateQueries({ queryKey: ['task-counters'] });
toast.success('Tarefa concluída com sucesso!');
},
onError: (error) => {
console.error('Error completing task:', error);
toast.error('Erro ao concluir tarefa');
},
});
// Archive task mutation
const { mutate: archiveTask, isPending: isArchiving } = useMutation({
mutationFn: async (id: string) => {
if (!user) throw new Error('User not authenticated');
const { data, error } = await supabase
.from('tasks')
.update({
archived_at: new Date().toISOString(),
archived_by: user.id,
updated_at: new Date().toISOString()
})
.eq('id', id)
.select()
.single();
if (error) throw error;
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] });
queryClient.invalidateQueries({ queryKey: ['task-counters'] });
toast.success('Tarefa arquivada com sucesso!');
},
onError: (error) => {
console.error('Error archiving task:', error);
toast.error('Erro ao arquivar tarefa');
},
});
// Delete task mutation
const { mutate: deleteTask, isPending: isDeleting } = useMutation({
mutationFn: async (id: string) => {
const { error } = await supabase
.from('tasks')
.delete()
.eq('id', id);
if (error) throw error;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] });
queryClient.invalidateQueries({ queryKey: ['task-counters'] });
toast.success('Tarefa deletada com sucesso!');
},
onError: (error) => {
console.error('Error deleting task:', error);
toast.error('Erro ao deletar tarefa');
},
});
return {
fetchMyTasks,
fetchMyCompletedTasks,
fetchAssignedTasks,
fetchTaskCounters,
availableUsers,
availableOFs,
acceptTask,
createTask,
updateTask,
completeTask,
archiveTask,
deleteTask,
isAccepting,
isCreating,
isUpdating,
isCompleting,
isArchiving,
isDeleting,
};
};
+108
View File
@@ -0,0 +1,108 @@
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import { supabase } from '@/integrations/supabase/client';
type Theme = 'dark' | 'light' | 'system';
interface ThemeProviderProps {
children: ReactNode;
defaultTheme?: Theme;
storageKey?: string;
}
interface ThemeProviderState {
theme: Theme;
setTheme: (theme: Theme) => void;
}
const initialState: ThemeProviderState = {
theme: 'light',
setTheme: () => null,
};
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
export function ThemeProvider({
children,
defaultTheme = 'light',
storageKey = 'tracksteel-ui-theme',
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(() => {
const stored = localStorage.getItem(storageKey) as Theme;
return stored || defaultTheme;
});
// Load theme configuration from database
const loadThemeConfig = async () => {
try {
const { data, error } = await supabase
.from('theme_config')
.select('*')
.limit(1)
.single();
if (data && !error) {
applyThemeColors(data);
}
} catch (error) {
console.error('Error loading theme config:', error);
}
};
const applyThemeColors = (config: any) => {
const root = window.document.documentElement;
const isDark = root.classList.contains('dark');
const themeColors = isDark ? config.dark_theme : config.light_theme;
if (themeColors) {
Object.entries(themeColors).forEach(([key, value]) => {
root.style.setProperty(`--${key}`, value as string);
});
}
};
useEffect(() => {
const root = window.document.documentElement;
// Remove all theme classes first
root.classList.remove('light', 'dark');
if (theme === 'system') {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
root.classList.add(systemTheme);
} else {
// Apply the selected theme immediately
root.classList.add(theme);
}
// Load custom theme colors after theme class is applied
loadThemeConfig();
}, [theme]);
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme);
setTheme(theme);
},
};
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
);
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext);
if (context === undefined)
throw new Error('useTheme must be used within a ThemeProvider');
return context;
};
+121
View File
@@ -0,0 +1,121 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
export interface ThemeColors {
background: string;
foreground: string;
primary: string;
'primary-foreground': string;
secondary: string;
'secondary-foreground': string;
accent: string;
'accent-foreground': string;
card: string;
'card-foreground': string;
border: string;
input: string;
ring: string;
}
export interface ThemeConfig {
id: string;
light_theme: ThemeColors;
dark_theme: ThemeColors;
}
export const useThemeConfig = () => {
const [themeConfig, setThemeConfig] = useState<ThemeConfig | null>(null);
const [isLoading, setIsLoading] = useState(true);
const fetchThemeConfig = async () => {
try {
const { data, error } = await supabase
.from('theme_config')
.select('*')
.limit(1)
.single();
if (error && error.code !== 'PGRST116') {
console.error('Error fetching theme config:', error);
toast.error('Erro ao carregar configurações de tema');
return;
}
if (data) {
// Type cast the JSON data to our expected format
const typedData: ThemeConfig = {
id: data.id,
light_theme: data.light_theme as unknown as ThemeColors,
dark_theme: data.dark_theme as unknown as ThemeColors,
};
setThemeConfig(typedData);
applyThemeToDocument(typedData);
}
} catch (error) {
console.error('Error:', error);
} finally {
setIsLoading(false);
}
};
const saveThemeConfig = async (lightTheme: ThemeColors, darkTheme: ThemeColors) => {
try {
const { data, error } = await supabase
.from('theme_config')
.upsert({
id: '00000000-0000-0000-0000-000000000001',
light_theme: lightTheme as any,
dark_theme: darkTheme as any,
})
.select()
.single();
if (error) {
console.error('Error saving theme config:', error);
toast.error('Erro ao salvar configurações de tema');
return false;
}
if (data) {
// Type cast the returned data
const typedData: ThemeConfig = {
id: data.id,
light_theme: data.light_theme as unknown as ThemeColors,
dark_theme: data.dark_theme as unknown as ThemeColors,
};
setThemeConfig(typedData);
applyThemeToDocument(typedData);
toast.success('Configurações de tema salvas com sucesso!');
return true;
}
} catch (error) {
console.error('Error:', error);
toast.error('Erro ao salvar configurações de tema');
return false;
}
return false;
};
const applyThemeToDocument = (config: ThemeConfig) => {
const root = document.documentElement;
const isDark = root.classList.contains('dark');
const theme = isDark ? config.dark_theme : config.light_theme;
Object.entries(theme).forEach(([key, value]) => {
root.style.setProperty(`--${key}`, value);
});
};
useEffect(() => {
fetchThemeConfig();
}, []);
return {
themeConfig,
isLoading,
saveThemeConfig,
applyThemeToDocument,
};
};
+35
View File
@@ -0,0 +1,35 @@
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
export interface TipoMaterial {
id: string;
nome: string;
descricao?: string;
ativo: boolean;
}
export const useTiposMateriais = () => {
const { data: tiposMateriais = [], isLoading } = useQuery({
queryKey: ['tipos-materiais'],
queryFn: async () => {
const { data, error } = await supabase
.from('tipos_materia_prima')
.select('*')
.eq('ativo', true)
.order('nome');
if (error) {
console.error('Erro ao buscar tipos de materiais:', error);
throw error;
}
return data as TipoMaterial[];
},
});
return {
tiposMateriais,
isLoading,
};
};
+43
View File
@@ -0,0 +1,43 @@
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { useAuth } from '@/hooks/useAuth';
export const useUserFunction = () => {
const { user } = useAuth();
const { data: userFunction, isLoading } = useQuery({
queryKey: ['user-function', user?.id],
queryFn: async () => {
if (!user?.id) return null;
const { data, error } = await supabase
.from('profiles')
.select(`
function_id,
functions (
name,
description
)
`)
.eq('id', user.id)
.single();
if (error) {
console.error('Erro ao buscar função do usuário:', error);
return null;
}
return data?.functions?.name || null;
},
enabled: !!user?.id,
});
const isComprador = userFunction?.toLowerCase().includes('comprador') || false;
return {
userFunction,
isComprador,
isLoading,
};
};
+477
View File
@@ -0,0 +1,477 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { useAuth } from './useAuth';
import { toast } from 'sonner';
import { useInterfaceResources } from './useInterfaceResources';
export interface UserProfile {
id: string;
email: string;
full_name: string | null;
profile_image_url: string | null;
function_id: string | null;
privilege_id: string | null;
status: 'pending' | 'active' | 'inactive' | 'rejected';
requested_at: string;
created_at: string;
updated_at: string;
functions?: {
name: string;
description: string;
};
privileges?: {
name: string;
description: string;
permissions: any;
};
}
export interface UserFunction {
id: string;
name: string;
description: string | null;
created_at: string;
updated_at: string;
}
export interface UserPrivilege {
id: string;
name: string;
description: string | null;
permissions: any;
created_at: string;
updated_at: string;
}
export interface UserDependency {
table_name: string;
dependency_type: string;
count: number;
description: string;
}
export function useUserManagement() {
const { user } = useAuth();
const [users, setUsers] = useState<UserProfile[]>([]);
const [pendingUsers, setPendingUsers] = useState<UserProfile[]>([]);
const [functions, setFunctions] = useState<UserFunction[]>([]);
const [privileges, setPrivileges] = useState<UserPrivilege[]>([]);
const [loading, setLoading] = useState(true);
const { updatePrivilegeResources } = useInterfaceResources();
// Fetch all users
const fetchUsers = async () => {
try {
const { data, error } = await supabase
.from('profiles')
.select(`
*,
functions (name, description),
privileges (name, description, permissions)
`)
.neq('status', 'pending')
.order('created_at', { ascending: false });
if (error) throw error;
setUsers(data || []);
} catch (error) {
console.error('Error fetching users:', error);
toast.error('Erro ao carregar usuários');
}
};
// Fetch pending users
const fetchPendingUsers = async () => {
try {
const { data, error } = await supabase
.from('profiles')
.select('*')
.eq('status', 'pending')
.order('requested_at', { ascending: false });
if (error) throw error;
setPendingUsers(data || []);
} catch (error) {
console.error('Error fetching pending users:', error);
toast.error('Erro ao carregar solicitações pendentes');
}
};
// Fetch functions
const fetchFunctions = async () => {
try {
const { data, error } = await supabase
.from('functions')
.select('*')
.order('name');
if (error) throw error;
setFunctions(data || []);
} catch (error) {
console.error('Error fetching functions:', error);
toast.error('Erro ao carregar funções');
}
};
// Fetch privileges
const fetchPrivileges = async () => {
try {
const { data, error } = await supabase
.from('privileges')
.select('*')
.order('name');
if (error) throw error;
setPrivileges(data || []);
} catch (error) {
console.error('Error fetching privileges:', error);
toast.error('Erro ao carregar privilégios');
}
};
// Get user dependencies
const getUserDependencies = async (userId: string): Promise<UserDependency[]> => {
try {
const { data, error } = await supabase.rpc('get_user_dependencies', {
_user_id: userId
});
if (error) throw error;
return data || [];
} catch (error) {
console.error('Error getting user dependencies:', error);
return [];
}
};
// Replace user with UserDel
const replaceUserWithDeleted = async (userId: string): Promise<boolean> => {
try {
const { error } = await supabase.rpc('replace_user_with_deleted', {
_user_id: userId
});
if (error) throw error;
return true;
} catch (error) {
console.error('Error replacing user with deleted:', error);
toast.error('Erro ao substituir referências do usuário');
return false;
}
};
// Create new user (Admin only)
const createUser = async (data: {
email: string;
full_name?: string;
function_id?: string;
privilege_id?: string;
}) => {
try {
const { data: result, error } = await supabase.rpc('admin_create_user', {
user_email: data.email,
user_full_name: data.full_name || null,
user_function_id: data.function_id || null,
user_privilege_id: data.privilege_id || null
});
if (error) throw error;
toast.success('Usuário criado com sucesso! Senha padrão: 1234');
fetchUsers();
return result;
} catch (error: any) {
console.error('Error creating user:', error);
if (error.message?.includes('User with this email already exists')) {
toast.error('Este e-mail já está em uso');
} else if (error.message?.includes('Only admins can create new users')) {
toast.error('Apenas administradores podem criar usuários');
} else {
toast.error('Erro ao criar usuário');
}
throw error;
}
};
// Check if user can be deleted
const canDeleteUser = async (userId: string): Promise<boolean> => {
try {
const { data, error } = await supabase.rpc('can_delete_user', {
_user_id: userId
});
if (error) throw error;
return data;
} catch (error) {
console.error('Error checking if user can be deleted:', error);
return false;
}
};
// Delete user (Admin only)
const deleteUser = async (userId: string, replaceReferences: boolean = false) => {
try {
// If replaceReferences is true, replace user references first
if (replaceReferences) {
const replaced = await replaceUserWithDeleted(userId);
if (!replaced) {
return false;
}
}
// First check if user can be deleted
const canDelete = await canDeleteUser(userId);
if (!canDelete) {
toast.error('Este usuário não pode ser excluído pois possui dados vinculados no sistema');
return false;
}
const { data, error } = await supabase.rpc('admin_delete_user', {
_user_id: userId
});
if (error) throw error;
toast.success('Usuário excluído com sucesso!');
fetchUsers();
fetchPendingUsers();
return true;
} catch (error: any) {
console.error('Error deleting user:', error);
if (error.message?.includes('Only admins can delete users')) {
toast.error('Apenas administradores podem excluir usuários');
} else if (error.message?.includes('User cannot be deleted due to existing dependencies')) {
toast.error('Este usuário não pode ser excluído pois possui dados vinculados no sistema');
} else {
toast.error('Erro ao excluir usuário');
}
return false;
}
};
// Approve user
const approveUser = async (userId: string) => {
try {
const { error } = await supabase
.from('profiles')
.update({ status: 'active' })
.eq('id', userId);
if (error) throw error;
toast.success('Usuário aprovado com sucesso!');
fetchUsers();
fetchPendingUsers();
} catch (error) {
console.error('Error approving user:', error);
toast.error('Erro ao aprovar usuário');
}
};
// Reject user
const rejectUser = async (userId: string) => {
try {
const { error } = await supabase
.from('profiles')
.update({ status: 'rejected' })
.eq('id', userId);
if (error) throw error;
toast.success('Usuário rejeitado');
fetchUsers();
fetchPendingUsers();
} catch (error) {
console.error('Error rejecting user:', error);
toast.error('Erro ao rejeitar usuário');
}
};
// Update user
const updateUser = async (userId: string, data: Partial<UserProfile>) => {
try {
const { error } = await supabase
.from('profiles')
.update(data)
.eq('id', userId);
if (error) throw error;
toast.success('Usuário atualizado com sucesso!');
fetchUsers();
} catch (error) {
console.error('Error updating user:', error);
toast.error('Erro ao atualizar usuário');
}
};
// Toggle user status
const toggleUserStatus = async (userId: string, currentStatus: string) => {
const newStatus = currentStatus === 'active' ? 'inactive' : 'active';
await updateUser(userId, { status: newStatus as any });
};
// CRUD functions for Functions table
const createFunction = async (data: { name: string; description?: string }) => {
try {
const { error } = await supabase
.from('functions')
.insert(data);
if (error) throw error;
toast.success('Função criada com sucesso!');
fetchFunctions();
} catch (error) {
console.error('Error creating function:', error);
toast.error('Erro ao criar função');
}
};
const updateFunction = async (id: string, data: { name: string; description?: string }) => {
try {
const { error } = await supabase
.from('functions')
.update(data)
.eq('id', id);
if (error) throw error;
toast.success('Função atualizada com sucesso!');
fetchFunctions();
} catch (error) {
console.error('Error updating function:', error);
toast.error('Erro ao atualizar função');
}
};
const deleteFunction = async (id: string) => {
try {
const { error } = await supabase
.from('functions')
.delete()
.eq('id', id);
if (error) throw error;
toast.success('Função removida com sucesso!');
fetchFunctions();
} catch (error) {
console.error('Error deleting function:', error);
toast.error('Erro ao remover função');
}
};
// CRUD functions for Privileges table - Updated to handle interface resources
const createPrivilege = async (data: { name: string; description?: string; permissions?: any }, resourceKeys: string[] = []) => {
try {
const { data: newPrivilege, error } = await supabase
.from('privileges')
.insert({
...data,
permissions: data.permissions || {}
})
.select()
.single();
if (error) throw error;
// Update interface resources if provided
if (resourceKeys.length > 0 && newPrivilege) {
await updatePrivilegeResources(newPrivilege.id, resourceKeys);
}
toast.success('Privilégio criado com sucesso!');
fetchPrivileges();
return newPrivilege;
} catch (error) {
console.error('Error creating privilege:', error);
toast.error('Erro ao criar privilégio');
throw error;
}
};
const updatePrivilege = async (id: string, data: { name: string; description?: string; permissions?: any }) => {
try {
const { error } = await supabase
.from('privileges')
.update(data)
.eq('id', id);
if (error) throw error;
toast.success('Privilégio atualizado com sucesso!');
fetchPrivileges();
} catch (error) {
console.error('Error updating privilege:', error);
toast.error('Erro ao atualizar privilégio');
}
};
const deletePrivilege = async (id: string) => {
try {
const { error } = await supabase
.from('privileges')
.delete()
.eq('id', id);
if (error) throw error;
toast.success('Privilégio removido com sucesso!');
fetchPrivileges();
} catch (error) {
console.error('Error deleting privilege:', error);
toast.error('Erro ao remover privilégio');
}
};
useEffect(() => {
if (user) {
const loadData = async () => {
setLoading(true);
await Promise.all([
fetchUsers(),
fetchPendingUsers(),
fetchFunctions(),
fetchPrivileges()
]);
setLoading(false);
};
loadData();
}
}, [user]);
return {
users,
pendingUsers,
functions,
privileges,
loading,
createUser,
approveUser,
rejectUser,
updateUser,
toggleUserStatus,
deleteUser,
canDeleteUser,
getUserDependencies,
replaceUserWithDeleted,
createFunction,
updateFunction,
deleteFunction,
createPrivilege,
updatePrivilege,
deletePrivilege,
refetchData: () => {
fetchUsers();
fetchPendingUsers();
fetchFunctions();
fetchPrivileges();
}
};
}
+4
View File
@@ -0,0 +1,4 @@
// Re-export from the new modular structure
export { useUserPermissions } from './useUserPermissions/useUserPermissions';
export type { UserPermissions, ActionType, FunctionalPermission } from './useUserPermissions/types';
+12
View File
@@ -0,0 +1,12 @@
export { useUserPermissions } from './useUserPermissions';
export type {
UserPermissions,
ActionType,
FunctionalPermission,
PermissionLevel,
ResourcePermission,
UserInterfacePermission
} from './types';
export { cleanPermissions } from './utils';
export { VALID_FUNCTIONAL_PERMISSIONS } from './types';
+62
View File
@@ -0,0 +1,62 @@
import { Database } from "@/integrations/supabase/types";
export type Profiles = Database['public']['Tables']['profiles']['Row']
// Permission level type - incluindo 'no_access'
export type PermissionLevel = 'can_admin' | 'can_create_update_delete' | 'can_create_only' | 'can_view_only' | 'no_access';
// User permissions interface
export interface UserPermissions {
can_admin: boolean;
can_create_update_delete: boolean;
can_create_only: boolean;
can_view_only: boolean;
}
// Action types for permission checking
export type ActionType = 'create' | 'read' | 'update' | 'delete' | 'admin';
// Resource key type for identifying different system resources
export type ResourceKey = string;
// Functional permission type
export type FunctionalPermission = keyof UserPermissions;
// User role types
export type UserRole = 'admin' | 'gerencia' | 'diretoria' | 'user';
export type AppRole = UserRole;
// Resource permission interface
export interface ResourcePermission {
resource_key: string;
permission_level: PermissionLevel;
}
// User interface permission interface
export interface UserInterfacePermission {
user_id: string;
resource_key: string;
permission_level: PermissionLevel;
created_at: string;
updated_at: string;
profiles?: {
email: string;
full_name: string | null;
};
}
// User with permissions interface (simplified without non-existent tables)
export interface UserWithPermissions extends Profiles {
permissions?: {
permission_level: PermissionLevel;
}[];
}
// Valid functional permissions constant
export const VALID_FUNCTIONAL_PERMISSIONS: FunctionalPermission[] = [
'can_admin',
'can_create_update_delete',
'can_create_only',
'can_view_only'
];
@@ -0,0 +1,256 @@
import { useState, useEffect, useCallback } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { useAuth } from '@/hooks/useAuth';
import { useUserRole } from '@/hooks/useUserRole';
import { toast } from 'sonner';
import type {
UserPermissions,
ResourceKey,
ActionType,
PermissionLevel,
FunctionalPermission
} from './types';
export function useUserPermissions() {
const { user } = useAuth();
const { isAdmin } = useUserRole();
const [userPermissions, setUserPermissions] = useState<UserPermissions>({
can_admin: false,
can_create_update_delete: false,
can_create_only: false,
can_view_only: false
});
const [resourcePermissions, setResourcePermissions] = useState<Record<string, PermissionLevel>>({});
const [loading, setLoading] = useState(true);
// Load user permissions
const loadUserPermissions = useCallback(async () => {
if (!user?.id) {
setLoading(false);
return;
}
try {
console.log(`🔄 Loading permissions for user: ${user.email}`);
// Load functional permissions from user privilege
const { data: profile } = await supabase
.from('profiles')
.select(`
privilege_id,
privileges!inner (
name,
permissions
)
`)
.eq('id', user.id)
.single();
if (profile?.privileges?.permissions) {
// Safe type conversion with validation
const permissions = profile.privileges.permissions;
if (typeof permissions === 'object' && permissions !== null && !Array.isArray(permissions)) {
const userPerms: UserPermissions = {
can_admin: Boolean(permissions.can_admin),
can_create_update_delete: Boolean(permissions.can_create_update_delete),
can_create_only: Boolean(permissions.can_create_only),
can_view_only: Boolean(permissions.can_view_only)
};
setUserPermissions(userPerms);
console.log('✅ Functional permissions loaded:', userPerms);
}
}
// Load specific resource permissions
const { data: resourcePerms } = await supabase
.from('user_interface_permissions')
.select('resource_key, permission')
.eq('user_id', user.id);
if (resourcePerms) {
const permsMap = resourcePerms.reduce((acc, perm) => {
acc[perm.resource_key] = perm.permission as PermissionLevel;
return acc;
}, {} as Record<string, PermissionLevel>);
setResourcePermissions(permsMap);
console.log('✅ Resource permissions loaded:', permsMap);
}
} catch (error) {
console.error('❌ Error loading permissions:', error);
} finally {
setLoading(false);
}
}, [user?.id]);
useEffect(() => {
loadUserPermissions();
}, [loadUserPermissions]);
// Check if user has access to a resource
const hasAccess = useCallback((resourceKey?: string): boolean => {
// Admin always has access
if (isAdmin) {
console.log(`✅ Admin access granted for resource: ${resourceKey || 'general'}`);
return true;
}
// If no resource specified, check general functional permissions
if (!resourceKey) {
const hasGeneralAccess = Object.values(userPermissions).some(Boolean);
console.log(`🔍 General access check: ${hasGeneralAccess}`, userPermissions);
return hasGeneralAccess;
}
// Check specific resource permission first
if (resourcePermissions[resourceKey]) {
const hasSpecificAccess = resourcePermissions[resourceKey] !== 'no_access';
console.log(`🎯 Specific resource permission for ${resourceKey}: ${resourcePermissions[resourceKey]} -> ${hasSpecificAccess}`);
return hasSpecificAccess;
}
// For equipamentos specifically, check if user has any functional permissions
if (resourceKey === 'equipamentos') {
const hasEquipamentosAccess = userPermissions.can_admin ||
userPermissions.can_create_update_delete ||
userPermissions.can_create_only ||
userPermissions.can_view_only;
console.log(`🔧 Equipamentos access check: ${hasEquipamentosAccess}`, userPermissions);
return hasEquipamentosAccess;
}
// Fallback to functional permissions
const hasFunctionalAccess = Object.values(userPermissions).some(Boolean);
console.log(`🔄 Fallback functional access for ${resourceKey}: ${hasFunctionalAccess}`, userPermissions);
return hasFunctionalAccess;
}, [isAdmin, userPermissions, resourcePermissions]);
// Check if user can access a specific route
const canAccessRoute = useCallback((route: string): boolean => {
// Map routes to resource keys
const routeToResource: Record<string, string> = {
'/equipamentos': 'equipamentos',
'/ferramentas/inconsistencias': 'ferramentas-inconsistencias',
'/admin': 'admin',
'/user-management': 'user-management',
'/sistema': 'sistema',
'/sugestoes': 'sugestoes',
'/tarefas': 'tarefas'
};
const resourceKey = routeToResource[route];
if (resourceKey) {
return hasAccess(resourceKey);
}
// Default to general access for unmapped routes
return hasAccess();
}, [hasAccess]);
// Get permission level for a specific resource
const getResourcePermission = useCallback((resourceKey: ResourceKey): PermissionLevel => {
// Admin always has full permissions
if (isAdmin) return 'can_admin';
// Check specific resource permission
if (resourcePermissions[resourceKey]) {
return resourcePermissions[resourceKey];
}
// Convert functional permissions to resource permission level
if (userPermissions.can_admin) return 'can_admin';
if (userPermissions.can_create_update_delete) return 'can_create_update_delete';
if (userPermissions.can_create_only) return 'can_create_only';
if (userPermissions.can_view_only) return 'can_view_only';
return 'no_access';
}, [isAdmin, userPermissions, resourcePermissions]);
// Check if user can perform a specific action on a resource
const canPerformActionByResource = useCallback((
action: ActionType,
resourceKey: ResourceKey
): boolean => {
const permission = getResourcePermission(resourceKey);
switch (permission) {
case 'can_admin':
return true;
case 'can_create_update_delete':
return action !== 'admin';
case 'can_create_only':
return action === 'create' || action === 'read';
case 'can_view_only':
return action === 'read';
case 'no_access':
return false;
default:
return false;
}
}, [getResourcePermission]);
// Generic permission check for functional permissions
const canPerformAction = useCallback((action: ActionType): boolean => {
if (isAdmin) return true;
switch (action) {
case 'admin':
return userPermissions.can_admin;
case 'create':
case 'update':
case 'delete':
return userPermissions.can_admin ||
userPermissions.can_create_update_delete ||
(action === 'create' && userPermissions.can_create_only);
case 'read':
return Object.values(userPermissions).some(Boolean);
default:
return false;
}
}, [isAdmin, userPermissions]);
// Set specific resource permission
const setResourcePermission = useCallback(async (
resourceKey: ResourceKey,
permission: PermissionLevel
): Promise<void> => {
if (!user?.id) return;
try {
const { error } = await supabase
.from('user_interface_permissions')
.upsert({
user_id: user.id,
resource_key: resourceKey,
permission: permission
});
if (error) throw error;
// Update local state
setResourcePermissions(prev => ({
...prev,
[resourceKey]: permission
}));
toast.success('Permissão atualizada com sucesso!');
} catch (error) {
console.error('Error setting resource permission:', error);
toast.error('Erro ao atualizar permissão');
}
}, [user?.id]);
return {
userPermissions,
resourcePermissions,
loading,
hasAccess,
canAccessRoute,
getResourcePermission,
canPerformAction,
canPerformActionByResource,
setResourcePermission,
refetch: loadUserPermissions
};
}
+37
View File
@@ -0,0 +1,37 @@
import { VALID_FUNCTIONAL_PERMISSIONS, UserPermissions } from './types';
// Function to clean permissions and keep only valid functional ones
export const cleanPermissions = (permissions: any): UserPermissions => {
if (!permissions || typeof permissions !== 'object') {
return {
can_admin: false,
can_create_update_delete: false,
can_create_only: false,
can_view_only: true // Default
};
}
const cleanedPermissions: UserPermissions = {
can_admin: false,
can_create_update_delete: false,
can_create_only: false,
can_view_only: false
};
// Maintain only valid functional permissions
VALID_FUNCTIONAL_PERMISSIONS.forEach(perm => {
if (permissions.hasOwnProperty(perm)) {
cleanedPermissions[perm] = Boolean(permissions[perm]);
}
});
// If no valid permission was found, set default
const hasAnyValidPermission = Object.values(cleanedPermissions).some(Boolean);
if (!hasAnyValidPermission) {
cleanedPermissions.can_view_only = true;
}
console.log('🧹 Cleaned permissions:', { original: permissions, cleaned: cleanedPermissions });
return cleanedPermissions;
};

Some files were not shown because too many files have changed in this diff Show More