🚀 Initial commit: Versão atual do TrackSteel APP
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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'));
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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'));
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -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');
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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()
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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'
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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';
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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');
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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');
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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');
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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');
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
// Re-export from the new modular structure
|
||||
export { useUserPermissions } from './useUserPermissions/useUserPermissions';
|
||||
export type { UserPermissions, ActionType, FunctionalPermission } from './useUserPermissions/types';
|
||||
@@ -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';
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user