Compare commits

...

2 Commits

10 changed files with 186 additions and 101 deletions
+2
View File
@@ -0,0 +1,2 @@
// Script temporário de auditoria da OF B134
export {};
+31 -13
View File
@@ -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"
+13 -14
View File
@@ -9,7 +9,6 @@ import {
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Checkbox } from '@/components/ui/checkbox';
import {
AlertTriangle,
@@ -224,7 +223,7 @@ export const ImportarPecasDiffModal: React.FC<ImportarPecasDiffModalProps> = ({
return (
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-6xl max-h-[92vh] flex flex-col p-0 overflow-hidden bg-background border-border">
<DialogContent className="max-w-6xl h-[90vh] max-h-[92vh] flex flex-col p-0 overflow-hidden bg-background border-border">
{/* Cabeçalho */}
<DialogHeader className="p-4 border-b border-border bg-card flex-shrink-0">
<div className="flex items-center justify-between">
@@ -367,7 +366,7 @@ export const ImportarPecasDiffModal: React.FC<ImportarPecasDiffModalProps> = ({
</DialogHeader>
{/* Barra de Filtros e Ferramentas */}
<div className="p-3 border-b border-border bg-muted/20 flex flex-wrap items-center justify-between gap-2 text-xs">
<div className="p-3 border-b border-border bg-muted/20 flex flex-wrap items-center justify-between gap-2 text-xs flex-shrink-0">
{/* Input de Busca */}
<div className="relative w-72">
<Search className="absolute left-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground" />
@@ -411,18 +410,18 @@ export const ImportarPecasDiffModal: React.FC<ImportarPecasDiffModalProps> = ({
</div>
</div>
{/* Tabela de Conciliação e Diff */}
<ScrollArea className="flex-1">
{/* Tabela de Conciliação e Diff com Scroll Vertical e Cabeçalho Sticky */}
<div className="flex-1 min-h-0 overflow-y-auto overflow-x-auto w-full">
<div className="p-4">
<table className="w-full text-left text-xs border-collapse">
<thead>
<tr className="border-b border-border text-muted-foreground font-semibold bg-muted/40">
<th className="p-2 w-10 text-center">Sobrepor</th>
<th className="p-2 w-28">Peça / Fase</th>
<th className="p-2 w-44">Perfil / Bitola</th>
<th className="p-2 w-40">Status de Produção</th>
<th className="p-2">Divergências Detectadas (Diff)</th>
<th className="p-2 w-28 text-center">Lembrete Fabril</th>
<thead className="sticky top-0 z-10 bg-muted/95 backdrop-blur-sm shadow-sm">
<tr className="border-b border-border text-muted-foreground font-semibold">
<th className="p-2 w-10 text-center bg-muted/95">Sobrepor</th>
<th className="p-2 w-28 bg-muted/95">Peça / Fase</th>
<th className="p-2 w-44 bg-muted/95">Perfil / Bitola</th>
<th className="p-2 w-40 bg-muted/95">Status de Produção</th>
<th className="p-2 bg-muted/95">Divergências Detectadas (Diff)</th>
<th className="p-2 w-28 text-center bg-muted/95">Lembrete Fabril</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
@@ -623,7 +622,7 @@ export const ImportarPecasDiffModal: React.FC<ImportarPecasDiffModalProps> = ({
</tbody>
</table>
</div>
</ScrollArea>
</div>
{/* Rodapé da Janela com Decisão e Ações */}
<DialogFooter className="p-3 border-t border-border bg-card flex flex-row items-center justify-between flex-shrink-0">
+5 -6
View File
@@ -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
+95
View File
@@ -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
+12 -27
View File
@@ -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,
+4 -7
View File
@@ -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) {
+6 -1
View File
@@ -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 });
+13 -31
View File
@@ -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
});
}
}
}