feat: precedencia sequencial de processos e regra pecas S/M (sem montagem)
This commit is contained in:
@@ -46,6 +46,7 @@ interface PecaStatus {
|
|||||||
pintura: number;
|
pintura: number;
|
||||||
expedido: number;
|
expedido: number;
|
||||||
montado_obra: number;
|
montado_obra: number;
|
||||||
|
tem_componentes?: boolean | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SmartConsultaRelatoriosFlow: React.FC<SmartConsultaRelatoriosFlowProps> = ({
|
export const SmartConsultaRelatoriosFlow: React.FC<SmartConsultaRelatoriosFlowProps> = ({
|
||||||
@@ -81,7 +82,7 @@ export const SmartConsultaRelatoriosFlow: React.FC<SmartConsultaRelatoriosFlowPr
|
|||||||
// 1. Peças da OF
|
// 1. Peças da OF
|
||||||
supabase
|
supabase
|
||||||
.from('pecas')
|
.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),
|
.eq('of_number', obra.of_number),
|
||||||
|
|
||||||
// 2. Processos de fabricação (para mapear nome sem depender de foreign key)
|
// 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,
|
pintura: procs.pintura,
|
||||||
expedido: mapaExp.get(p.marca) || 0,
|
expedido: mapaExp.get(p.marca) || 0,
|
||||||
montado_obra: mapaMont.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}
|
{peca.marca}
|
||||||
</span>
|
</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 && (
|
{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">
|
<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}
|
{peca.perfil_principal}
|
||||||
@@ -905,14 +913,30 @@ export const SmartConsultaRelatoriosFlow: React.FC<SmartConsultaRelatoriosFlowPr
|
|||||||
{/* Linha do Tempo Visual com TODAS as Etapas Fabris */}
|
{/* 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">
|
<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: 'Det.', valor: peca.detalhamento, isPula: false },
|
||||||
{ nome: 'Corte', valor: peca.corte },
|
{ nome: 'Corte', valor: peca.corte, isPula: false },
|
||||||
{ nome: 'Mont.', valor: peca.montagem },
|
{ nome: 'Mont.', valor: peca.montagem, isPula: peca.tem_componentes === false },
|
||||||
{ nome: 'Solda', valor: peca.solda },
|
{ nome: 'Solda', valor: peca.solda, isPula: peca.tem_componentes === false },
|
||||||
{ nome: 'Pint.', valor: peca.pintura },
|
{ nome: 'Pint.', valor: peca.pintura, isPula: false },
|
||||||
{ nome: 'Emb.', valor: peca.expedido },
|
{ nome: 'Emb.', valor: peca.expedido, isPula: false },
|
||||||
{ nome: 'Obra', valor: peca.montado_obra },
|
{ nome: 'Obra', valor: peca.montado_obra, isPula: false },
|
||||||
].map((etapa, idx) => {
|
].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);
|
const badge = getBadgeEtapa(etapa.valor, peca.quantidade);
|
||||||
return (
|
return (
|
||||||
<div key={idx} className="flex flex-col items-center">
|
<div key={idx} className="flex flex-col items-center">
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ interface ItemRomaneioCarregado {
|
|||||||
quantidade_expedida: number;
|
quantidade_expedida: number;
|
||||||
peso_unitario: number;
|
peso_unitario: number;
|
||||||
peso_total: number;
|
peso_total: number;
|
||||||
|
tem_componentes?: boolean | null;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +68,7 @@ interface PecaPronta {
|
|||||||
peso_unitario: number;
|
peso_unitario: number;
|
||||||
quantidadeJaExpedida: number;
|
quantidadeJaExpedida: number;
|
||||||
saldoDisponivel: number;
|
saldoDisponivel: number;
|
||||||
|
tem_componentes?: boolean | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubTelaEmbarque =
|
type SubTelaEmbarque =
|
||||||
@@ -159,7 +161,7 @@ export const SmartEmbarqueFlow: React.FC<SmartEmbarqueFlowProps> = ({ obra, onVo
|
|||||||
const [resPecas, resItensExpedidos] = await Promise.all([
|
const [resPecas, resItensExpedidos] = await Promise.all([
|
||||||
supabase
|
supabase
|
||||||
.from('pecas')
|
.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),
|
.eq('of_number', obra.of_number),
|
||||||
supabase
|
supabase
|
||||||
.from('itens_romaneio_pecas')
|
.from('itens_romaneio_pecas')
|
||||||
@@ -982,9 +984,16 @@ export const SmartEmbarqueFlow: React.FC<SmartEmbarqueFlowProps> = ({ obra, onVo
|
|||||||
{pecaEmbarque ? (
|
{pecaEmbarque ? (
|
||||||
<div className="p-4 rounded-2xl bg-slate-800 border-2 border-blue-500/80 mb-3 shadow-lg">
|
<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">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<span className="text-xl font-black text-amber-400">
|
<div className="flex items-center gap-2">
|
||||||
{pecaEmbarque.marca}
|
<span className="text-xl font-black text-amber-400">
|
||||||
</span>
|
{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">
|
<span className="text-xs font-bold text-slate-400">
|
||||||
Saldo Disponível: {pecaEmbarque.saldoDisponivel} un
|
Saldo Disponível: {pecaEmbarque.saldoDisponivel} un
|
||||||
</span>
|
</span>
|
||||||
@@ -1081,6 +1090,11 @@ export const SmartEmbarqueFlow: React.FC<SmartEmbarqueFlowProps> = ({ obra, onVo
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-lg font-black text-amber-400">{p.marca}</span>
|
<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">
|
<span className="text-[11px] px-2 py-0.5 rounded bg-slate-700 text-slate-300 font-semibold">
|
||||||
{p.etapa_fase || 'Estrutural'}
|
{p.etapa_fase || 'Estrutural'}
|
||||||
</span>
|
</span>
|
||||||
@@ -1266,6 +1280,11 @@ export const SmartEmbarqueFlow: React.FC<SmartEmbarqueFlowProps> = ({ obra, onVo
|
|||||||
<span className="text-base font-black text-amber-400">
|
<span className="text-base font-black text-amber-400">
|
||||||
{item.marca}
|
{item.marca}
|
||||||
</span>
|
</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">
|
<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'}
|
{item.fase || '1'}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ interface ItemMontagem {
|
|||||||
quantidade_expedida: number;
|
quantidade_expedida: number;
|
||||||
quantidade_ja_apontada: number;
|
quantidade_ja_apontada: number;
|
||||||
saldo_disponivel: number;
|
saldo_disponivel: number;
|
||||||
|
tem_componentes?: boolean | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SmartMontagemFlow: React.FC<SmartMontagemFlowProps> = ({
|
export const SmartMontagemFlow: React.FC<SmartMontagemFlowProps> = ({
|
||||||
@@ -50,24 +51,41 @@ export const SmartMontagemFlow: React.FC<SmartMontagemFlowProps> = ({
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
// 1. Buscar peças expedidas em romaneios desta OF
|
// Buscar peças expedidas, apontamentos RDO e info de componentes em paralelo
|
||||||
const { data: pecasExpedidas, error: errorExp } = await supabase
|
const [resExp, resRDO, resPecas] = await Promise.all([
|
||||||
.from('itens_romaneio_pecas')
|
supabase
|
||||||
.select(`
|
.from('itens_romaneio_pecas')
|
||||||
marca,
|
.select(`
|
||||||
descricao,
|
marca,
|
||||||
peso_unitario,
|
descricao,
|
||||||
quantidade_expedida,
|
peso_unitario,
|
||||||
romaneios_expedicao!inner(
|
quantidade_expedida,
|
||||||
of_number
|
romaneios_expedicao!inner(
|
||||||
)
|
of_number
|
||||||
`)
|
)
|
||||||
.eq('romaneios_expedicao.of_number', obra.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 }>();
|
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) || {
|
const atual = mapaExpedidas.get(item.marca) || {
|
||||||
marca: item.marca,
|
marca: item.marca,
|
||||||
descricao: item.descricao || '',
|
descricao: item.descricao || '',
|
||||||
@@ -78,26 +96,19 @@ export const SmartMontagemFlow: React.FC<SmartMontagemFlowProps> = ({
|
|||||||
mapaExpedidas.set(item.marca, atual);
|
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>();
|
const mapaMontadas = new Map<string, number>();
|
||||||
(apontamentosRDO || []).forEach((ap: any) => {
|
(resRDO.data || []).forEach((ap: any) => {
|
||||||
const atual = mapaMontadas.get(ap.marca_peca) || 0;
|
const atual = mapaMontadas.get(ap.marca_peca) || 0;
|
||||||
mapaMontadas.set(ap.marca_peca, atual + Number(ap.quantidade || 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
|
// 3. Montar lista de saldos
|
||||||
const lista: ItemMontagem[] = Array.from(mapaExpedidas.values()).map((exp) => {
|
const lista: ItemMontagem[] = Array.from(mapaExpedidas.values()).map((exp) => {
|
||||||
const jaMontado = mapaMontadas.get(exp.marca) || 0;
|
const jaMontado = mapaMontadas.get(exp.marca) || 0;
|
||||||
@@ -109,6 +120,7 @@ export const SmartMontagemFlow: React.FC<SmartMontagemFlowProps> = ({
|
|||||||
quantidade_expedida: exp.expedido,
|
quantidade_expedida: exp.expedido,
|
||||||
quantidade_ja_apontada: jaMontado,
|
quantidade_ja_apontada: jaMontado,
|
||||||
saldo_disponivel: saldo,
|
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 */}
|
{/* Resumo da Peça */}
|
||||||
<div className="p-4 rounded-2xl bg-slate-800/90 border border-slate-700 mb-4 shadow-sm">
|
<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">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-2xl font-black text-amber-400">
|
<div className="flex items-center gap-2">
|
||||||
{itemSelecionado.marca}
|
<span className="text-2xl font-black text-amber-400">
|
||||||
</span>
|
{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">
|
<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
|
Montagem em Obra
|
||||||
</span>
|
</span>
|
||||||
@@ -388,9 +407,16 @@ export const SmartMontagemFlow: React.FC<SmartMontagemFlowProps> = ({
|
|||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-xl font-black text-amber-400 tracking-wider">
|
<div className="flex items-center gap-2">
|
||||||
{item.marca}
|
<span className="text-xl font-black text-amber-400 tracking-wider">
|
||||||
</span>
|
{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">
|
<div className="text-xs text-slate-300 mt-0.5 line-clamp-1">
|
||||||
{item.descricao || 'Peça Estrutural'}
|
{item.descricao || 'Peça Estrutural'}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,8 +38,80 @@ interface PecaData {
|
|||||||
quantidade: number;
|
quantidade: number;
|
||||||
peso_unitario: number;
|
peso_unitario: number;
|
||||||
perfil_principal?: string;
|
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 {
|
interface ApontamentoItem {
|
||||||
peca_id: string;
|
peca_id: string;
|
||||||
processo_id: string;
|
processo_id: string;
|
||||||
@@ -77,7 +149,7 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
|||||||
const [resPecas, resApontamentos] = await Promise.all([
|
const [resPecas, resApontamentos] = await Promise.all([
|
||||||
supabase
|
supabase
|
||||||
.from('pecas')
|
.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),
|
.eq('of_number', obra.of_number),
|
||||||
supabase
|
supabase
|
||||||
.from('apontamentos_producao')
|
.from('apontamentos_producao')
|
||||||
@@ -126,7 +198,7 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
|||||||
return mapa;
|
return mapa;
|
||||||
}, [apontamentosExistentes]);
|
}, [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(() => {
|
const pecasComSaldo = useMemo(() => {
|
||||||
if (!processoSelecionado) return [];
|
if (!processoSelecionado) return [];
|
||||||
|
|
||||||
@@ -136,22 +208,19 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
|||||||
return p.etapa_fase?.trim() === faseSelecionada;
|
return p.etapa_fase?.trim() === faseSelecionada;
|
||||||
})
|
})
|
||||||
.map((p) => {
|
.map((p) => {
|
||||||
const key = `${p.id}_${processoSelecionado.id}`;
|
const info = calcularSaldoProcessoPeca(p, processoSelecionado, processos, mapaProducao);
|
||||||
const jaProduzido = mapaProducao.get(key) || 0;
|
|
||||||
const saldo = Math.max(0, p.quantidade - jaProduzido);
|
|
||||||
return {
|
return {
|
||||||
...p,
|
...p,
|
||||||
jaProduzido,
|
...info,
|
||||||
saldo,
|
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.sort((a, b) => {
|
.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;
|
||||||
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' });
|
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
|
// Helper de ícone por processo
|
||||||
const getIconeProcesso = (nome: string) => {
|
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">
|
<span className="text-xl font-black text-amber-400 tracking-wider">
|
||||||
{item.marca}
|
{item.marca}
|
||||||
</span>
|
</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 && (
|
{item.perfil_principal && (
|
||||||
<span className="text-xs px-2 py-0.5 rounded-md bg-slate-700/80 text-slate-300 font-semibold">
|
<span className="text-xs px-2 py-0.5 rounded-md bg-slate-700/80 text-slate-300 font-semibold">
|
||||||
{item.perfil_principal}
|
{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">
|
<div className="text-xs text-slate-300 mt-0.5 line-clamp-1">
|
||||||
{item.descricao || 'Peça Estrutural'}
|
{item.descricao || 'Peça Estrutural'}
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div className="text-right shrink-0">
|
<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">
|
<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
|
<Check className="h-3 w-3" /> 100% Pronto
|
||||||
</span>
|
</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">
|
<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>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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">
|
<div className="mt-3 grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={concluida}
|
disabled={concluida || item.saldo <= 0}
|
||||||
onClick={() => handleIniciarApontamentoPeca(item, item.saldo)}
|
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] ${
|
className={`sm:col-span-2 h-13 text-sm font-black rounded-xl uppercase tracking-wider transition-all active:scale-[0.98] ${
|
||||||
concluida
|
concluida
|
||||||
? 'bg-slate-800 text-slate-500 border border-slate-700'
|
? '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'
|
: '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>
|
</Button>
|
||||||
|
|
||||||
{!concluida && (
|
{!concluida && item.saldo > 0 && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -550,7 +643,7 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
|||||||
setEtapaAtual('quantidade');
|
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"
|
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})
|
TODAS ({item.saldo})
|
||||||
</Button>
|
</Button>
|
||||||
@@ -581,9 +674,13 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
|||||||
// RENDER: ETAPA 1.4 — APONTAMENTO DA QUANTIDADE (TECLADO TOUCH + INPUT DIRETO)
|
// RENDER: ETAPA 1.4 — APONTAMENTO DA QUANTIDADE (TECLADO TOUCH + INPUT DIRETO)
|
||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
if (etapaAtual === 'quantidade' && pecaSelecionada && processoSelecionado) {
|
if (etapaAtual === 'quantidade' && pecaSelecionada && processoSelecionado) {
|
||||||
const key = `${pecaSelecionada.id}_${processoSelecionado.id}`;
|
const infoSaldoAtual = calcularSaldoProcessoPeca(
|
||||||
const jaProduzido = mapaProducao.get(key) || 0;
|
pecaSelecionada,
|
||||||
const saldoPendente = Math.max(0, pecaSelecionada.quantidade - jaProduzido);
|
processoSelecionado,
|
||||||
|
processos,
|
||||||
|
mapaProducao
|
||||||
|
);
|
||||||
|
const saldoPendente = infoSaldoAtual.saldo;
|
||||||
|
|
||||||
// Ajuste por incremento
|
// Ajuste por incremento
|
||||||
const somarQtd = (valor: number) => {
|
const somarQtd = (valor: number) => {
|
||||||
@@ -651,9 +748,19 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
|||||||
{/* Resumo da Peça no Topo */}
|
{/* 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="p-3.5 rounded-2xl bg-slate-800/90 border border-slate-700 mb-3 shadow-sm">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xl font-black text-amber-400">
|
<div className="flex items-center gap-2">
|
||||||
{pecaSelecionada.marca}
|
<span className="text-xl font-black text-amber-400">
|
||||||
</span>
|
{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">
|
<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}
|
{processoSelecionado.nome}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user