feat: precedencia sequencial de processos e regra pecas S/M (sem montagem)

This commit is contained in:
2026-09-21 23:48:04 +00:00
parent db76f87ef9
commit 6eb04ac79e
4 changed files with 245 additions and 69 deletions
@@ -46,6 +46,7 @@ interface PecaStatus {
pintura: number;
expedido: number;
montado_obra: number;
tem_componentes?: boolean | null;
}
export const SmartConsultaRelatoriosFlow: React.FC<SmartConsultaRelatoriosFlowProps> = ({
@@ -81,7 +82,7 @@ export const SmartConsultaRelatoriosFlow: React.FC<SmartConsultaRelatoriosFlowPr
// 1. Peças da OF
supabase
.from('pecas')
.select('id, marca, descricao, etapa_fase, quantidade, peso_unitario, perfil_principal, material')
.select('id, marca, descricao, etapa_fase, quantidade, peso_unitario, perfil_principal, material, tem_componentes')
.eq('of_number', obra.of_number),
// 2. Processos de fabricação (para mapear nome sem depender de foreign key)
@@ -196,6 +197,7 @@ export const SmartConsultaRelatoriosFlow: React.FC<SmartConsultaRelatoriosFlowPr
pintura: procs.pintura,
expedido: mapaExp.get(p.marca) || 0,
montado_obra: mapaMont.get(p.marca) || 0,
tem_componentes: p.tem_componentes,
};
});
@@ -871,6 +873,12 @@ export const SmartConsultaRelatoriosFlow: React.FC<SmartConsultaRelatoriosFlowPr
{peca.marca}
</span>
{peca.tem_componentes === false && (
<span className="text-[10px] px-2 py-0.5 rounded-md bg-purple-950/80 text-purple-300 font-bold border border-purple-800/50">
S/M
</span>
)}
{peca.perfil_principal && (
<span className="text-xs px-2 py-0.5 rounded-md bg-amber-500/15 text-amber-300 font-black border border-amber-500/30">
{peca.perfil_principal}
@@ -905,14 +913,30 @@ export const SmartConsultaRelatoriosFlow: React.FC<SmartConsultaRelatoriosFlowPr
{/* Linha do Tempo Visual com TODAS as Etapas Fabris */}
<div className="grid grid-cols-7 gap-1 mt-3 pt-2.5 border-t border-slate-700/80 text-center">
{[
{ nome: 'Det.', valor: peca.detalhamento },
{ nome: 'Corte', valor: peca.corte },
{ nome: 'Mont.', valor: peca.montagem },
{ nome: 'Solda', valor: peca.solda },
{ nome: 'Pint.', valor: peca.pintura },
{ nome: 'Emb.', valor: peca.expedido },
{ nome: 'Obra', valor: peca.montado_obra },
{ nome: 'Det.', valor: peca.detalhamento, isPula: false },
{ nome: 'Corte', valor: peca.corte, isPula: false },
{ nome: 'Mont.', valor: peca.montagem, isPula: peca.tem_componentes === false },
{ nome: 'Solda', valor: peca.solda, isPula: peca.tem_componentes === false },
{ nome: 'Pint.', valor: peca.pintura, isPula: false },
{ nome: 'Emb.', valor: peca.expedido, isPula: false },
{ nome: 'Obra', valor: peca.montado_obra, isPula: false },
].map((etapa, idx) => {
if (etapa.isPula) {
return (
<div key={idx} className="flex flex-col items-center opacity-60">
<div className="text-[9px] font-bold text-slate-400 mb-0.5 uppercase">
{etapa.nome}
</div>
<div className="h-5 px-1 rounded-md bg-slate-900 border border-purple-800/40 mb-0.5 flex items-center justify-center">
<span className="text-[9px] font-bold text-purple-400">S/M</span>
</div>
<span className="text-[10px] font-bold text-slate-500">
N/A
</span>
</div>
);
}
const badge = getBadgeEtapa(etapa.valor, peca.quantidade);
return (
<div key={idx} className="flex flex-col items-center">
@@ -55,6 +55,7 @@ interface ItemRomaneioCarregado {
quantidade_expedida: number;
peso_unitario: number;
peso_total: number;
tem_componentes?: boolean | null;
created_at?: string;
}
@@ -67,6 +68,7 @@ interface PecaPronta {
peso_unitario: number;
quantidadeJaExpedida: number;
saldoDisponivel: number;
tem_componentes?: boolean | null;
}
type SubTelaEmbarque =
@@ -159,7 +161,7 @@ export const SmartEmbarqueFlow: React.FC<SmartEmbarqueFlowProps> = ({ obra, onVo
const [resPecas, resItensExpedidos] = await Promise.all([
supabase
.from('pecas')
.select('id, marca, descricao, etapa_fase, quantidade, peso_unitario')
.select('id, marca, descricao, etapa_fase, quantidade, peso_unitario, tem_componentes')
.eq('of_number', obra.of_number),
supabase
.from('itens_romaneio_pecas')
@@ -982,9 +984,16 @@ export const SmartEmbarqueFlow: React.FC<SmartEmbarqueFlowProps> = ({ obra, onVo
{pecaEmbarque ? (
<div className="p-4 rounded-2xl bg-slate-800 border-2 border-blue-500/80 mb-3 shadow-lg">
<div className="flex items-center justify-between mb-2">
<span className="text-xl font-black text-amber-400">
{pecaEmbarque.marca}
</span>
<div className="flex items-center gap-2">
<span className="text-xl font-black text-amber-400">
{pecaEmbarque.marca}
</span>
{pecaEmbarque.tem_componentes === false && (
<span className="text-[10px] px-2 py-0.5 rounded-md bg-purple-950/80 text-purple-300 font-bold border border-purple-800/50">
S/M
</span>
)}
</div>
<span className="text-xs font-bold text-slate-400">
Saldo Disponível: {pecaEmbarque.saldoDisponivel} un
</span>
@@ -1081,6 +1090,11 @@ export const SmartEmbarqueFlow: React.FC<SmartEmbarqueFlowProps> = ({ obra, onVo
<div>
<div className="flex items-center gap-2">
<span className="text-lg font-black text-amber-400">{p.marca}</span>
{p.tem_componentes === false && (
<span className="text-[10px] px-2 py-0.5 rounded-md bg-purple-950/80 text-purple-300 font-bold border border-purple-800/50">
S/M
</span>
)}
<span className="text-[11px] px-2 py-0.5 rounded bg-slate-700 text-slate-300 font-semibold">
{p.etapa_fase || 'Estrutural'}
</span>
@@ -1266,6 +1280,11 @@ export const SmartEmbarqueFlow: React.FC<SmartEmbarqueFlowProps> = ({ obra, onVo
<span className="text-base font-black text-amber-400">
{item.marca}
</span>
{(item.tem_componentes === false || pecasProntas.some(p => p.id === item.peca_id && p.tem_componentes === false)) && (
<span className="text-[10px] px-2 py-0.5 rounded bg-purple-950/80 text-purple-300 font-bold border border-purple-800/40">
S/M
</span>
)}
<span className="text-[10px] px-2 py-0.5 rounded bg-blue-950/80 text-blue-300 font-bold border border-blue-800/40">
{item.fase || '1'}
</span>
@@ -28,6 +28,7 @@ interface ItemMontagem {
quantidade_expedida: number;
quantidade_ja_apontada: number;
saldo_disponivel: number;
tem_componentes?: boolean | null;
}
export const SmartMontagemFlow: React.FC<SmartMontagemFlowProps> = ({
@@ -50,24 +51,41 @@ export const SmartMontagemFlow: React.FC<SmartMontagemFlowProps> = ({
try {
setLoading(true);
// 1. Buscar peças expedidas em romaneios desta OF
const { data: pecasExpedidas, error: errorExp } = await supabase
.from('itens_romaneio_pecas')
.select(`
marca,
descricao,
peso_unitario,
quantidade_expedida,
romaneios_expedicao!inner(
of_number
)
`)
.eq('romaneios_expedicao.of_number', obra.of_number);
// Buscar peças expedidas, apontamentos RDO e info de componentes em paralelo
const [resExp, resRDO, resPecas] = await Promise.all([
supabase
.from('itens_romaneio_pecas')
.select(`
marca,
descricao,
peso_unitario,
quantidade_expedida,
romaneios_expedicao!inner(
of_number
)
`)
.eq('romaneios_expedicao.of_number', obra.of_number),
supabase
.from('apontamentos_peca_obra')
.select(`
marca_peca,
quantidade,
diario_obra_rdo!inner(
of_number
)
`)
.eq('diario_obra_rdo.of_number', obra.of_number),
supabase
.from('pecas')
.select('marca, tem_componentes')
.eq('of_number', obra.of_number),
]);
if (errorExp) throw errorExp;
if (resExp.error) throw resExp.error;
if (resRDO.error) throw resRDO.error;
const mapaExpedidas = new Map<string, { marca: string; descricao: string; peso_unitario: number; expedido: number }>();
(pecasExpedidas || []).forEach((item: any) => {
(resExp.data || []).forEach((item: any) => {
const atual = mapaExpedidas.get(item.marca) || {
marca: item.marca,
descricao: item.descricao || '',
@@ -78,26 +96,19 @@ export const SmartMontagemFlow: React.FC<SmartMontagemFlowProps> = ({
mapaExpedidas.set(item.marca, atual);
});
// 2. Buscar apontamentos já feitos no RDO para esta OF
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', obra.of_number);
if (errorRDO) throw errorRDO;
const mapaMontadas = new Map<string, number>();
(apontamentosRDO || []).forEach((ap: any) => {
(resRDO.data || []).forEach((ap: any) => {
const atual = mapaMontadas.get(ap.marca_peca) || 0;
mapaMontadas.set(ap.marca_peca, atual + Number(ap.quantidade || 0));
});
const mapaSM = new Map<string, boolean>();
(resPecas.data || []).forEach((p: any) => {
if (p.tem_componentes === false) {
mapaSM.set(p.marca, true);
}
});
// 3. Montar lista de saldos
const lista: ItemMontagem[] = Array.from(mapaExpedidas.values()).map((exp) => {
const jaMontado = mapaMontadas.get(exp.marca) || 0;
@@ -109,6 +120,7 @@ export const SmartMontagemFlow: React.FC<SmartMontagemFlowProps> = ({
quantidade_expedida: exp.expedido,
quantidade_ja_apontada: jaMontado,
saldo_disponivel: saldo,
tem_componentes: mapaSM.has(exp.marca) ? false : true,
};
});
@@ -218,9 +230,16 @@ export const SmartMontagemFlow: React.FC<SmartMontagemFlowProps> = ({
{/* Resumo da Peça */}
<div className="p-4 rounded-2xl bg-slate-800/90 border border-slate-700 mb-4 shadow-sm">
<div className="flex items-center justify-between">
<span className="text-2xl font-black text-amber-400">
{itemSelecionado.marca}
</span>
<div className="flex items-center gap-2">
<span className="text-2xl font-black text-amber-400">
{itemSelecionado.marca}
</span>
{itemSelecionado.tem_componentes === false && (
<span className="text-[10px] px-2 py-0.5 rounded-md bg-purple-950/80 text-purple-300 font-bold border border-purple-800/50">
S/M
</span>
)}
</div>
<span className="text-xs font-black px-2.5 py-1 rounded-lg bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 uppercase">
Montagem em Obra
</span>
@@ -388,9 +407,16 @@ export const SmartMontagemFlow: React.FC<SmartMontagemFlowProps> = ({
>
<div className="flex items-start justify-between gap-2">
<div>
<span className="text-xl font-black text-amber-400 tracking-wider">
{item.marca}
</span>
<div className="flex items-center gap-2">
<span className="text-xl font-black text-amber-400 tracking-wider">
{item.marca}
</span>
{item.tem_componentes === false && (
<span className="text-[10px] px-2 py-0.5 rounded-md bg-purple-950/80 text-purple-300 font-bold border border-purple-800/50">
S/M
</span>
)}
</div>
<div className="text-xs text-slate-300 mt-0.5 line-clamp-1">
{item.descricao || 'Peça Estrutural'}
</div>
+128 -21
View File
@@ -38,8 +38,80 @@ interface PecaData {
quantidade: number;
peso_unitario: number;
perfil_principal?: string;
tem_componentes?: boolean | null;
}
interface CalculoSaldoPeca {
saldo: number;
jaProduzido: number;
qtdDisponivelParaEntrar: number;
ehSemMontagem: boolean;
motivoBloqueio: string | null;
}
// Função auxiliar pura para cálculo sequencial de saldo por processo e regra S/M
export const calcularSaldoProcessoPeca = (
peca: PecaData,
processoAtual: ProcessoFabricacao,
todosProcessos: ProcessoFabricacao[],
mapaProd: Map<string, number>
): CalculoSaldoPeca => {
const ehSemMontagem = peca.tem_componentes === false;
const keyAtual = `${peca.id}_${processoAtual.id}`;
const jaProduzidoAtual = mapaProd.get(keyAtual) || 0;
const procsOrdenados = [...todosProcessos].sort((a, b) => a.ordem - b.ordem);
const indexAtual = procsOrdenados.findIndex((p) => p.id === processoAtual.id);
const nomeProcAtual = (processoAtual.nome || '').toLowerCase();
const ehSolda = nomeProcAtual.includes('solda');
const ehMontagem = nomeProcAtual.includes('montag');
let qtdDisponivelParaEntrar = Number(peca.quantidade) || 0;
let motivoBloqueio: string | null = null;
// REGRA 1: Peças sem montagem (S/M) não passam por Montagem nem por Solda
if (ehSemMontagem && (ehSolda || ehMontagem)) {
qtdDisponivelParaEntrar = 0;
motivoBloqueio = 'Peça S/M (Pula Montagem e Solda)';
} else if (indexAtual > 0) {
// REGRA 2: Encontrar processo anterior válido
let procAnteriorValido: ProcessoFabricacao | null = null;
for (let i = indexAtual - 1; i >= 0; i--) {
const proc = procsOrdenados[i];
const nomeP = proc.nome.toLowerCase();
// Se a peça for S/M, pula solda e montagem na busca do anterior (ex: Corte -> Pintura)
if (ehSemMontagem && (nomeP.includes('solda') || nomeP.includes('montag'))) {
continue;
}
procAnteriorValido = proc;
break;
}
if (procAnteriorValido) {
const keyAnt = `${peca.id}_${procAnteriorValido.id}`;
const qtdApontadaAnterior = mapaProd.get(keyAnt) || 0;
qtdDisponivelParaEntrar = qtdApontadaAnterior;
if (qtdApontadaAnterior === 0) {
motivoBloqueio = `Aguardando ${procAnteriorValido.nome}`;
} else if (qtdApontadaAnterior < peca.quantidade && qtdApontadaAnterior <= jaProduzidoAtual) {
motivoBloqueio = `Aguardando saldo em ${procAnteriorValido.nome} (${qtdApontadaAnterior}/${peca.quantidade})`;
}
}
}
const saldo = Math.max(0, qtdDisponivelParaEntrar - jaProduzidoAtual);
return {
saldo,
jaProduzido: jaProduzidoAtual,
qtdDisponivelParaEntrar,
ehSemMontagem,
motivoBloqueio,
};
};
interface ApontamentoItem {
peca_id: string;
processo_id: string;
@@ -77,7 +149,7 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
const [resPecas, resApontamentos] = await Promise.all([
supabase
.from('pecas')
.select('id, marca, descricao, etapa_fase, quantidade, peso_unitario, perfil_principal')
.select('id, marca, descricao, etapa_fase, quantidade, peso_unitario, perfil_principal, tem_componentes')
.eq('of_number', obra.of_number),
supabase
.from('apontamentos_producao')
@@ -126,7 +198,7 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
return mapa;
}, [apontamentosExistentes]);
// Peças filtradas pela fase selecionada com cálculo do saldo
// Peças filtradas pela fase selecionada com cálculo estrito de precedência de processos e regra S/M
const pecasComSaldo = useMemo(() => {
if (!processoSelecionado) return [];
@@ -136,22 +208,19 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
return p.etapa_fase?.trim() === faseSelecionada;
})
.map((p) => {
const key = `${p.id}_${processoSelecionado.id}`;
const jaProduzido = mapaProducao.get(key) || 0;
const saldo = Math.max(0, p.quantidade - jaProduzido);
const info = calcularSaldoProcessoPeca(p, processoSelecionado, processos, mapaProducao);
return {
...p,
jaProduzido,
saldo,
...info,
};
})
.sort((a, b) => {
// Colocar primeiro as que têm saldo pendente
// Colocar primeiro as que têm saldo pendente disponível > 0
if (a.saldo > 0 && b.saldo === 0) return -1;
if (a.saldo === 0 && b.saldo > 0) return 1;
return a.marca.localeCompare(b.marca, undefined, { numeric: true, sensitivity: 'base' });
});
}, [pecas, faseSelecionada, processoSelecionado, mapaProducao]);
}, [pecas, faseSelecionada, processoSelecionado, mapaProducao, processos]);
// Helper de ícone por processo
const getIconeProcesso = (nome: string) => {
@@ -488,6 +557,14 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
<span className="text-xl font-black text-amber-400 tracking-wider">
{item.marca}
</span>
{item.tem_componentes === false && (
<span
className="text-[10px] px-2 py-0.5 rounded-md bg-purple-950/80 text-purple-300 font-bold border border-purple-800/50"
title="Peça Sem Montagem (Pula Solda e Montagem)"
>
S/M
</span>
)}
{item.perfil_principal && (
<span className="text-xs px-2 py-0.5 rounded-md bg-slate-700/80 text-slate-300 font-semibold">
{item.perfil_principal}
@@ -497,6 +574,12 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
<div className="text-xs text-slate-300 mt-0.5 line-clamp-1">
{item.descricao || 'Peça Estrutural'}
</div>
{item.motivoBloqueio && item.saldo === 0 && !concluida && (
<div className="text-[11px] text-amber-400/90 font-medium mt-1 flex items-center gap-1">
<AlertTriangle className="h-3.5 w-3.5 shrink-0 text-amber-400" />
<span>{item.motivoBloqueio}</span>
</div>
)}
</div>
<div className="text-right shrink-0">
@@ -505,9 +588,13 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
<span className="inline-flex items-center gap-1 text-xs font-black text-emerald-400 px-2 py-0.5 rounded-full bg-emerald-950/80 border border-emerald-700">
<Check className="h-3 w-3" /> 100% Pronto
</span>
) : (
) : item.saldo > 0 ? (
<span className="inline-flex items-center gap-1 text-xs font-black text-amber-300 px-2 py-0.5 rounded-full bg-amber-950/80 border border-amber-700">
Falta: {item.saldo}
Disponível: {item.saldo}
</span>
) : (
<span className="inline-flex items-center gap-1 text-[11px] font-bold text-slate-400 px-2 py-0.5 rounded-full bg-slate-800 border border-slate-700">
Pendente ant.
</span>
)}
</div>
@@ -529,18 +616,24 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
<div className="mt-3 grid grid-cols-1 sm:grid-cols-3 gap-2">
<Button
type="button"
disabled={concluida}
disabled={concluida || item.saldo <= 0}
onClick={() => handleIniciarApontamentoPeca(item, item.saldo)}
className={`sm:col-span-2 h-13 text-sm font-black rounded-xl uppercase tracking-wider transition-all active:scale-[0.98] ${
concluida
? 'bg-slate-800 text-slate-500 border border-slate-700'
: item.saldo <= 0
? 'bg-slate-800 text-slate-400 border border-slate-700 cursor-not-allowed'
: 'bg-amber-500 hover:bg-amber-400 active:bg-amber-600 text-slate-950 shadow-md shadow-amber-950/30'
}`}
>
{concluida ? 'Processo Concluído' : `DIGITAR / ESCOLHER QUANTIDADE`}
{concluida
? 'Processo Concluído'
: item.saldo <= 0
? item.motivoBloqueio || 'Indisponível neste processo'
: `DIGITAR / ESCOLHER QUANTIDADE`}
</Button>
{!concluida && (
{!concluida && item.saldo > 0 && (
<Button
type="button"
onClick={() => {
@@ -550,7 +643,7 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
setEtapaAtual('quantidade');
}}
className="h-13 bg-slate-700 hover:bg-slate-650 active:bg-slate-600 text-amber-400 border border-slate-600 text-xs font-black rounded-xl uppercase active:scale-95"
title="Apontar todo o saldo restante de uma vez"
title="Apontar todo o saldo disponível de uma vez"
>
TODAS ({item.saldo})
</Button>
@@ -581,9 +674,13 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
// RENDER: ETAPA 1.4 — APONTAMENTO DA QUANTIDADE (TECLADO TOUCH + INPUT DIRETO)
// ─────────────────────────────────────────────────────────────
if (etapaAtual === 'quantidade' && pecaSelecionada && processoSelecionado) {
const key = `${pecaSelecionada.id}_${processoSelecionado.id}`;
const jaProduzido = mapaProducao.get(key) || 0;
const saldoPendente = Math.max(0, pecaSelecionada.quantidade - jaProduzido);
const infoSaldoAtual = calcularSaldoProcessoPeca(
pecaSelecionada,
processoSelecionado,
processos,
mapaProducao
);
const saldoPendente = infoSaldoAtual.saldo;
// Ajuste por incremento
const somarQtd = (valor: number) => {
@@ -651,9 +748,19 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
{/* Resumo da Peça no Topo */}
<div className="p-3.5 rounded-2xl bg-slate-800/90 border border-slate-700 mb-3 shadow-sm">
<div className="flex items-center justify-between">
<span className="text-xl font-black text-amber-400">
{pecaSelecionada.marca}
</span>
<div className="flex items-center gap-2">
<span className="text-xl font-black text-amber-400">
{pecaSelecionada.marca}
</span>
{pecaSelecionada.tem_componentes === false && (
<span
className="text-[10px] px-2 py-0.5 rounded-md bg-purple-950/80 text-purple-300 font-bold border border-purple-800/50"
title="Peça Sem Montagem (Pula Solda e Montagem)"
>
S/M
</span>
)}
</div>
<span className="text-xs font-black px-2.5 py-0.5 rounded-lg bg-amber-500/20 text-amber-300 border border-amber-500/30 uppercase">
{processoSelecionado.nome}
</span>