feat: blindagem de peso em dashboards e kit completo de corte para solda
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
// Script temporário de auditoria da OF B134
|
||||
export {};
|
||||
@@ -118,6 +118,8 @@ export const ApontamentoForm = () => {
|
||||
const processosOrdenados = [...processos].sort((a, b) => a.ordem - b.ordem);
|
||||
const indexAtual = processosOrdenados.findIndex(p => p.id === processo_id);
|
||||
const procAtual = processosOrdenados[indexAtual];
|
||||
const procNomeLower = (procAtual?.nome || '').toLowerCase();
|
||||
const isCorte = procNomeLower.includes('corte') || procNomeLower.includes('prepar') || indexAtual === 0;
|
||||
|
||||
// 1. Peças da OF e Fase selecionadas
|
||||
const pecasDaFase = pecas.filter(
|
||||
@@ -134,8 +136,13 @@ export const ApontamentoForm = () => {
|
||||
|
||||
let qtdDisponivelParaEntrar = Number(peca.quantidade) || 0;
|
||||
|
||||
// Regra: peças sem componentes não passam por solda
|
||||
if (!peca.tem_componentes && procAtual && procAtual.nome.toLowerCase().includes('solda')) {
|
||||
// Se for CORTE:
|
||||
// Peças compostas são produzidas através do corte de seus subcomponentes.
|
||||
// Ao atingir 100% dos componentes cortados, o kit é concluído e a peça mestre é liberada para Solda.
|
||||
if (isCorte && peca.tem_componentes) {
|
||||
qtdDisponivelParaEntrar = 0;
|
||||
} else if (!peca.tem_componentes && procNomeLower.includes('solda')) {
|
||||
// Regra: peças sem componentes não passam por solda
|
||||
qtdDisponivelParaEntrar = 0;
|
||||
} else if (indexAtual > 0) {
|
||||
// Encontrar processo anterior válido
|
||||
@@ -150,9 +157,27 @@ export const ApontamentoForm = () => {
|
||||
}
|
||||
|
||||
if (processoAnteriorValido) {
|
||||
qtdDisponivelParaEntrar = apontamentos
|
||||
let qtdApontadaAnterior = apontamentos
|
||||
.filter(a => a.tipo_apontamento === 'peca' && a.peca_id === peca.id && a.processo_id === processoAnteriorValido.id)
|
||||
.reduce((sum, a) => sum + (Number(a.quantidade_produzida) || 0), 0);
|
||||
|
||||
// Fallback de retrocompatibilidade para peças com componentes cortados antes da automação
|
||||
if (peca.tem_componentes && qtdApontadaAnterior === 0 && processoAnteriorValido.nome.toLowerCase().includes('corte')) {
|
||||
const compsDaPeca = componentesAgrupados?.filter(c => c.peca_ids?.includes(peca.id)) || [];
|
||||
if (compsDaPeca.length > 0) {
|
||||
const todosCortados = compsDaPeca.every(c => {
|
||||
const aptsComp = apontamentos
|
||||
.filter(a => a.tipo_apontamento === 'componente' && c.componente_ids.includes(a.componente_id || '') && a.processo_id === processoAnteriorValido.id)
|
||||
.reduce((sum, a) => sum + (Number(a.quantidade_produzida) || 0), 0);
|
||||
return aptsComp >= c.quantidade_total;
|
||||
});
|
||||
if (todosCortados) {
|
||||
qtdApontadaAnterior = Number(peca.quantidade) || 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
qtdDisponivelParaEntrar = qtdApontadaAnterior;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,21 +198,14 @@ export const ApontamentoForm = () => {
|
||||
});
|
||||
|
||||
// 2. Componentes da OF e Fase selecionadas
|
||||
// RESTRIÇÃO INDUSTRIAL: Componentes só existem no processo de CORTE / PREPARAÇÃO!
|
||||
// Em Solda, Pintura, Expedição, Montagem etc., componentes não são exibidos nem apontados.
|
||||
const componentesDisponiveis: ItemDisponivel[] = [];
|
||||
|
||||
if (componentesAgrupados && componentesAgrupados.length > 0) {
|
||||
if (isCorte && componentesAgrupados && componentesAgrupados.length > 0) {
|
||||
componentesAgrupados.forEach(comp => {
|
||||
let qtdDisponivelParaEntrarComp = Number(comp.quantidade_total) || 0;
|
||||
|
||||
if (indexAtual > 0) {
|
||||
const processoAnteriorValido = processosOrdenados[indexAtual - 1];
|
||||
if (processoAnteriorValido) {
|
||||
qtdDisponivelParaEntrarComp = apontamentos
|
||||
.filter(a => a.tipo_apontamento === 'componente' && comp.componente_ids.includes(a.componente_id || '') && a.processo_id === processoAnteriorValido.id)
|
||||
.reduce((sum, a) => sum + (Number(a.quantidade_produzida) || 0), 0);
|
||||
}
|
||||
}
|
||||
|
||||
const totalApontadoComp = apontamentos
|
||||
.filter(a => a.tipo_apontamento === 'componente' && comp.componente_ids.includes(a.componente_id || '') && a.processo_id === processo_id)
|
||||
.reduce((sum, a) => sum + (Number(a.quantidade_produzida) || 0), 0);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { toast } from 'sonner';
|
||||
import { Search, Package, Shield, AlertTriangle, Settings, Check, Loader2 } from 'lucide-react';
|
||||
import { Search, Package, Shield, AlertTriangle, Settings, Check, Loader2, Layers } from 'lucide-react';
|
||||
import { useDebounce } from '@/hooks/useDebounce';
|
||||
import { Peca } from '@/hooks/usePecas';
|
||||
import { useComponentesPeca } from '@/hooks/useComponentesPeca';
|
||||
@@ -628,7 +628,10 @@ export const SeletorItensOtimizado: React.FC<SeletorItensOtimizadoProps> = ({
|
||||
{componentesDisponiveis.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-foreground">Componentes Disponíveis</h3>
|
||||
<h3 className="text-lg font-semibold text-foreground flex items-center gap-2">
|
||||
<Layers className="h-5 w-5 text-blue-500" />
|
||||
Componentes para Corte (Controle de Kit)
|
||||
</h3>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
|
||||
@@ -118,14 +118,13 @@ export const useApontamentoDiarioChart = (periodo: PeriodoType) => {
|
||||
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);
|
||||
// Blindagem contra dupla contagem: apenas peças mestres computam peso nos gráficos de produção.
|
||||
// Componentes são rastreados para controle de kit/corte, mas não somam peso na balança global.
|
||||
if (apontamento.tipo_apontamento !== 'peca') {
|
||||
return;
|
||||
}
|
||||
|
||||
const pesoUnitario = Number(apontamento.peca?.peso_unitario || 0);
|
||||
let pesoTotal = pesoUnitario * Number(apontamento.quantidade_produzida);
|
||||
|
||||
// Para projeção, calcular média diária e multiplicar por 7
|
||||
|
||||
@@ -378,6 +378,101 @@ interface InsertApontamentoData {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// REGRA DO KIT COMPLETO (Passo 3 + Opção 1):
|
||||
// Se apontou componente no processo de Corte, verificar se todos os componentes da peça pai completaram o kit.
|
||||
// Se completou, cria automaticamente o apontamento da Peça Principal no Corte (tipo_apontamento = 'peca'),
|
||||
// computando o peso integral da peça mestre no Corte e liberando-a para a Solda!
|
||||
if (insertData.tipo_apontamento === 'componente' && insertData.peca_id) {
|
||||
try {
|
||||
const procAtual = processos.find(p => p.id === insertData.processo_id);
|
||||
const isProcessoCorte = procAtual
|
||||
? (procAtual.nome.toLowerCase().includes('corte') || procAtual.nome.toLowerCase().includes('prepar') || procAtual.ordem === 1)
|
||||
: true;
|
||||
|
||||
if (isProcessoCorte) {
|
||||
const pecaId = insertData.peca_id;
|
||||
|
||||
// 1. Buscar todos os componentes requeridos da peça pai
|
||||
const { data: compsPeca } = await supabase
|
||||
.from('componentes_peca')
|
||||
.select('id, quantidade_por_peca')
|
||||
.eq('peca_id', pecaId);
|
||||
|
||||
// 2. Buscar a peça pai
|
||||
const { data: pecaPai } = await supabase
|
||||
.from('pecas')
|
||||
.select('id, marca, quantidade, of_number')
|
||||
.eq('id', pecaId)
|
||||
.single();
|
||||
|
||||
if (pecaPai && compsPeca && compsPeca.length > 0) {
|
||||
// 3. Buscar todos os apontamentos de componentes já feitos para essa peça pai neste processo de corte
|
||||
const { data: todosAptsComp } = await supabase
|
||||
.from('apontamentos_producao')
|
||||
.select('componente_id, quantidade_produzida')
|
||||
.eq('peca_id', pecaId)
|
||||
.eq('processo_id', insertData.processo_id)
|
||||
.eq('tipo_apontamento', 'componente');
|
||||
|
||||
const cortadoPorComp = new Map<string, number>();
|
||||
(todosAptsComp || []).forEach(a => {
|
||||
if (a.componente_id) {
|
||||
cortadoPorComp.set(a.componente_id, (cortadoPorComp.get(a.componente_id) || 0) + Number(a.quantidade_produzida || 0));
|
||||
}
|
||||
});
|
||||
|
||||
let kitsCompletos = Number(pecaPai.quantidade) || 0;
|
||||
for (const comp of compsPeca) {
|
||||
const cortado = cortadoPorComp.get(comp.id) || 0;
|
||||
const porPeca = Number(comp.quantidade_por_peca) || 1;
|
||||
const kitsDeste = Math.floor(cortado / porPeca);
|
||||
if (kitsDeste < kitsCompletos) {
|
||||
kitsCompletos = kitsDeste;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Verificar quanto já foi apontado como 'peca' mestre no corte
|
||||
const { data: aptsPecaJaRegistrados } = await supabase
|
||||
.from('apontamentos_producao')
|
||||
.select('quantidade_produzida')
|
||||
.eq('peca_id', pecaId)
|
||||
.eq('processo_id', insertData.processo_id)
|
||||
.eq('tipo_apontamento', 'peca');
|
||||
|
||||
const totalPecaJaApontada = (aptsPecaJaRegistrados || []).reduce(
|
||||
(sum, a) => sum + Number(a.quantidade_produzida || 0), 0
|
||||
);
|
||||
|
||||
// 5. Se há novos kits completos, registrar apontamento da peça mestre no Corte
|
||||
const qtdLiberar = kitsCompletos - totalPecaJaApontada;
|
||||
if (qtdLiberar > 0) {
|
||||
const { error: errPeca } = await supabase
|
||||
.from('apontamentos_producao')
|
||||
.insert({
|
||||
of_number: pecaPai.of_number,
|
||||
peca_id: pecaPai.id,
|
||||
processo_id: insertData.processo_id,
|
||||
tipo_apontamento: 'peca',
|
||||
quantidade_produzida: qtdLiberar,
|
||||
data_apontamento: insertData.data_apontamento,
|
||||
observacoes: `Liberação automática de Kit 100% cortado (${kitsCompletos}/${pecaPai.quantidade} un)`,
|
||||
created_by: user.id
|
||||
});
|
||||
|
||||
if (!errPeca) {
|
||||
console.log(`✅ Kit 100% completo! ${qtdLiberar} un da peça mestre ${pecaPai.marca} registradas no Corte.`);
|
||||
toast.success(`Kit 100% cortado! ${qtdLiberar} un da peça ${pecaPai.marca} liberada(s) para Solda.`);
|
||||
} else {
|
||||
console.warn('Aviso ao registrar liberação automática de peça:', errPeca);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (kitError) {
|
||||
console.error('Erro ao verificar regra de kit completo:', kitError);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ Apontamento criado com sucesso, forçando atualização completa...');
|
||||
|
||||
// FORÇA ATUALIZAÇÃO COMPLETA DOS DADOS
|
||||
|
||||
@@ -171,16 +171,11 @@ export const useDashboardProducaoOtimizado = (ofNumber: string) => {
|
||||
const processoCorteId = processoCorte.data?.id;
|
||||
const pecasInfoMap = new Map((pecasData || []).map(p => [p.id, p]));
|
||||
|
||||
// 1. Somar peças com componentes apontadas na Solda
|
||||
// 1. Somar peças com componentes apontadas na Solda (apenas tipo peca)
|
||||
if (processoSoldaId) {
|
||||
const apontamentosSolda = apontamentosData.filter(a => a.processo_id === processoSoldaId);
|
||||
const apontamentosSolda = apontamentosData.filter(a => a.processo_id === processoSoldaId && a.tipo_apontamento === 'peca');
|
||||
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 pesoUnitario = Number(apontamento.peca?.peso_unitario || 0);
|
||||
const pesoApontamento = pesoUnitario * Number(apontamento.quantidade_produzida || 0);
|
||||
pesoTotalFabricado += pesoApontamento;
|
||||
});
|
||||
@@ -357,15 +352,10 @@ export const useDashboardProducaoOtimizado = (ofNumber: string) => {
|
||||
|
||||
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);
|
||||
// Apenas apontamentos de peça mestre somam peso no processo para evitar dupla contagem com subcomponentes
|
||||
if (a.tipo_apontamento === 'peca' && a.peca?.peso_unitario) {
|
||||
pesoFabricadoProcesso += Number(a.peca.peso_unitario) * Number(a.quantidade_produzida);
|
||||
}
|
||||
|
||||
pesoFabricadoProcesso += pesoUnitario * Number(a.quantidade_produzida);
|
||||
});
|
||||
|
||||
const isProcessoSolda = processo.nome.toLowerCase().includes('solda');
|
||||
@@ -507,17 +497,12 @@ export const useDashboardProducaoOtimizado = (ofNumber: string) => {
|
||||
return dataAptStr <= 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);
|
||||
});
|
||||
apontamentosAteData.forEach(a => {
|
||||
// Apenas peças mestres computam peso realizado no gráfico
|
||||
if (a.tipo_apontamento === 'peca' && a.peca?.peso_unitario) {
|
||||
realizado += Number(a.peca.peso_unitario) * Number(a.quantidade_produzida);
|
||||
}
|
||||
});
|
||||
|
||||
dadosGrafico.push({
|
||||
data: dataStr,
|
||||
|
||||
@@ -81,14 +81,11 @@ export const useProcessChartData = (ofNumber: string) => {
|
||||
|
||||
if (apontamentos && apontamentos.length > 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;
|
||||
}
|
||||
// Apenas peças mestres somam peso nas médias diárias e metas
|
||||
if (apontamento.tipo_apontamento !== 'peca') return;
|
||||
|
||||
const pesoTotal = pesoUnitario * apontamento.quantidade_produzida;
|
||||
const pesoUnitario = Number(apontamento.peca?.peso_unitario || 0);
|
||||
const pesoTotal = pesoUnitario * Number(apontamento.quantidade_produzida);
|
||||
pesoTotalProcessado += pesoTotal;
|
||||
|
||||
if (apontamento.data_apontamento) {
|
||||
|
||||
@@ -12,6 +12,7 @@ interface ApontamentoDiario {
|
||||
descricao: string;
|
||||
processo: string;
|
||||
quantidade: number;
|
||||
tipo_apontamento: 'peca' | 'componente';
|
||||
peso_unitario: number;
|
||||
peso_total: number;
|
||||
data_apontamento: string;
|
||||
@@ -105,6 +106,7 @@ export const useRelatorioDiario = (selectedDate: string) => {
|
||||
descricao,
|
||||
processo: apt.processo?.nome || 'N/A',
|
||||
quantidade: apt.quantidade_produzida,
|
||||
tipo_apontamento: (apt.tipo_apontamento || 'peca') as 'peca' | 'componente',
|
||||
peso_unitario,
|
||||
peso_total,
|
||||
data_apontamento: apt.data_apontamento,
|
||||
@@ -139,11 +141,14 @@ export const useRelatorioDiario = (selectedDate: string) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedDate]);
|
||||
|
||||
// Calcular resumo por processo
|
||||
// Calcular resumo por processo (apenas peças principais computam quantidade e peso no resumo da produção)
|
||||
const resumoProcessos: ResumoProcesso[] = React.useMemo(() => {
|
||||
const resumo = new Map<string, { quantidade: number; apontamentos: number; peso_total: number }>();
|
||||
|
||||
apontamentos.forEach(apt => {
|
||||
// Ignorar componentes no resumo global de produção para evitar peso e contagem duplicados
|
||||
if (apt.tipo_apontamento !== 'peca') return;
|
||||
|
||||
const processo = apt.processo;
|
||||
if (!resumo.has(processo)) {
|
||||
resumo.set(processo, { quantidade: 0, apontamentos: 0, peso_total: 0 });
|
||||
|
||||
@@ -119,6 +119,15 @@ export const useValidacaoSequencialProcessos = () => {
|
||||
return { valido: true };
|
||||
}
|
||||
|
||||
// Componentes só podem ser processados no primeiro processo (Corte / Preparação)
|
||||
const itemEComponente = historico.some(h => h.marca_item === marcaItem && h.tipo_item === 'componente');
|
||||
if (itemEComponente) {
|
||||
return {
|
||||
valido: false,
|
||||
erro: `Componente ${marcaItem} não pode ser apontado após a etapa de Corte / Preparação.`
|
||||
};
|
||||
}
|
||||
|
||||
// Buscar histórico do item específico
|
||||
const historicoItem = historico.filter(h => h.marca_item === marcaItem);
|
||||
|
||||
@@ -238,41 +247,14 @@ export const useValidacaoSequencialProcessos = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Processar componentes
|
||||
for (const componente of componentes) {
|
||||
const marcaComp = String(componente.marca_componente || '');
|
||||
const qtdComp = Number(componente.quantidade_total || 0);
|
||||
|
||||
if (ordemAtual === 1) {
|
||||
// Primeiro processo: todos os componentes estão disponíveis
|
||||
// Processar componentes: apenas disponíveis no primeiro processo (Corte / Preparação)
|
||||
if (ordemAtual === 1) {
|
||||
for (const componente of componentes) {
|
||||
const qtdComp = Number(componente.quantidade_total || 0);
|
||||
componentesDisponiveis.push({
|
||||
...componente,
|
||||
quantidade_disponivel: qtdComp
|
||||
});
|
||||
} else {
|
||||
// Calcular quantidade disponível baseada no processo anterior
|
||||
const quantidadeProcessadaAnterior = await calcularQuantidadeDisponivelProcessoAnterior(
|
||||
marcaComp,
|
||||
ordemAtual - 1,
|
||||
ofNumber,
|
||||
'componente'
|
||||
);
|
||||
|
||||
const quantidadeJaProcessadaAtual = await buscarQuantidadeProcessada(
|
||||
marcaComp,
|
||||
processoId,
|
||||
ofNumber,
|
||||
'componente'
|
||||
);
|
||||
|
||||
const quantidadeDisponivel = quantidadeProcessadaAnterior - quantidadeJaProcessadaAtual;
|
||||
|
||||
if (quantidadeDisponivel > 0) {
|
||||
componentesDisponiveis.push({
|
||||
...componente,
|
||||
quantidade_disponivel: quantidadeDisponivel
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user