fix: corrigir processos vazios no item 4, adicionar filtros por perfil/fase e ordenacao natural das marcas

This commit is contained in:
2026-09-21 22:58:07 +00:00
parent e5c1439f19
commit 5902b6f8ef
2 changed files with 337 additions and 98 deletions
@@ -2,7 +2,6 @@ import React, { useState, useEffect, useMemo } from 'react';
import {
Search,
FileText,
Share2,
Download,
Mail,
X,
@@ -10,8 +9,10 @@ import {
Clock,
Circle,
AlertCircle,
Eye,
MessageCircle
MessageCircle,
Filter,
RotateCcw,
Layers
} from 'lucide-react';
import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable';
@@ -34,8 +35,11 @@ interface PecaStatus {
marca: string;
descricao: string;
etapa_fase: string;
perfil_principal?: string;
material?: string;
quantidade: number;
peso_unitario: number;
detalhamento: number;
corte: number;
montagem: number;
solda: number;
@@ -56,35 +60,42 @@ export const SmartConsultaRelatoriosFlow: React.FC<SmartConsultaRelatoriosFlowPr
abrirDiretoTurno ? 'relatorios' : 'consulta'
);
// Estados da Consulta
// Estados da Consulta e Filtros
const [busca, setBusca] = useState('');
const [filtroFase, setFiltroFase] = useState<string>('todas');
const [filtroPerfilRapido, setFiltroPerfilRapido] = useState<string>('');
const [filtroStatus, setFiltroStatus] = useState<'todos' | 'fabricando' | 'pronto' | 'embarcado' | 'montado'>('todos');
const [pecasStatus, setPecasStatus] = useState<PecaStatus[]>([]);
const [loading, setLoading] = useState(true);
// Estados de Relatórios
const [gerandoPDF, setGerandoPDF] = useState(false);
// Carregar dados de peças e progresso
// Carregar dados de peças, processos e apontamentos da OF
const carregarDadosCompletos = React.useCallback(async () => {
try {
setLoading(true);
const [resPecas, resApontamentos, resExpedidos, resMontados] = await Promise.all([
const [resPecas, resProcessos, resApontamentos, resExpedidos, resMontados] = await Promise.all([
// 1. Peças da OF
supabase
.from('pecas')
.select('id, marca, descricao, etapa_fase, quantidade, peso_unitario')
.select('id, marca, descricao, etapa_fase, quantidade, peso_unitario, perfil_principal, material')
.eq('of_number', obra.of_number),
// 2. Processos de fabricação (para mapear nome sem depender de foreign key)
supabase
.from('processos_fabricacao')
.select('id, nome, ordem'),
// 3. Apontamentos da OF
supabase
.from('apontamentos_producao')
.select(`
peca_id,
quantidade_produzida,
processos_fabricacao (
nome
)
`)
.select('peca_id, processo_id, quantidade_produzida')
.eq('of_number', obra.of_number),
// 4. Expedição em Romaneios
supabase
.from('itens_romaneio_pecas')
.select(`
@@ -93,6 +104,8 @@ export const SmartConsultaRelatoriosFlow: React.FC<SmartConsultaRelatoriosFlowPr
romaneios_expedicao!inner(of_number)
`)
.eq('romaneios_expedicao.of_number', obra.of_number),
// 5. Montagem em Obra (RDO)
supabase
.from('apontamentos_peca_obra')
.select(`
@@ -103,20 +116,44 @@ export const SmartConsultaRelatoriosFlow: React.FC<SmartConsultaRelatoriosFlowPr
.eq('diario_obra_rdo.of_number', obra.of_number),
]);
// Mapear apontamentos por peça e nome de processo
const mapaProc = new Map<string, { corte: number; montagem: number; solda: number; pintura: number }>();
// Mapear processos por ID -> Nome em minúsculas
const mapaNomeProcesso = new Map<string, string>();
(resProcessos.data || []).forEach((proc) => {
mapaNomeProcesso.set(proc.id, proc.nome.toLowerCase().trim());
});
// Mapear apontamentos por peca_id
const mapaProcPeca = new Map<
string,
{ detalhamento: number; corte: number; montagem: number; solda: number; pintura: number }
>();
(resApontamentos.data || []).forEach((ap: any) => {
const pecaId = ap.peca_id;
const nomeProc = (ap.processos_fabricacao?.nome || '').toLowerCase();
const nomeProc = mapaNomeProcesso.get(ap.processo_id) || '';
const qtd = Number(ap.quantidade_produzida || 0);
const atual = mapaProc.get(pecaId) || { corte: 0, montagem: 0, solda: 0, pintura: 0 };
if (nomeProc.includes('corte')) atual.corte += qtd;
else if (nomeProc.includes('montag')) atual.montagem += qtd;
else if (nomeProc.includes('solda')) atual.solda += qtd;
else if (nomeProc.includes('pintura')) atual.pintura += qtd;
const atual = mapaProcPeca.get(pecaId) || {
detalhamento: 0,
corte: 0,
montagem: 0,
solda: 0,
pintura: 0,
};
mapaProc.set(pecaId, atual);
if (nomeProc.includes('detalh')) {
atual.detalhamento += qtd;
} else if (nomeProc.includes('corte')) {
atual.corte += qtd;
} else if (nomeProc.includes('montag')) {
atual.montagem += qtd;
} else if (nomeProc.includes('solda')) {
atual.solda += qtd;
} else if (nomeProc.includes('pint') || nomeProc.includes('galv') || nomeProc.includes('jateam')) {
atual.pintura += qtd;
}
mapaProcPeca.set(pecaId, atual);
});
// Mapear expedição por marca
@@ -133,16 +170,26 @@ export const SmartConsultaRelatoriosFlow: React.FC<SmartConsultaRelatoriosFlowPr
mapaMont.set(marca, (mapaMont.get(marca) || 0) + Number(item.quantidade || 0));
});
// Montar PecaStatus
// Montar PecaStatus com ordenação natural alfanumérica
const lista: PecaStatus[] = (resPecas.data || []).map((p) => {
const procs = mapaProc.get(p.id) || { corte: 0, montagem: 0, solda: 0, pintura: 0 };
const procs = mapaProcPeca.get(p.id) || {
detalhamento: 0,
corte: 0,
montagem: 0,
solda: 0,
pintura: 0,
};
return {
id: p.id,
marca: p.marca,
descricao: p.descricao,
etapa_fase: p.etapa_fase,
quantidade: p.quantidade,
peso_unitario: p.peso_unitario,
marca: p.marca || '',
descricao: p.descricao || '',
etapa_fase: p.etapa_fase || '1',
perfil_principal: p.perfil_principal || '',
material: p.material || '',
quantidade: Number(p.quantidade || 0),
peso_unitario: Number(p.peso_unitario || 0),
detalhamento: procs.detalhamento,
corte: procs.corte,
montagem: procs.montagem,
solda: procs.solda,
@@ -152,6 +199,11 @@ export const SmartConsultaRelatoriosFlow: React.FC<SmartConsultaRelatoriosFlowPr
};
});
// ORDENAÇÃO NATURAL: 1, 2, 3... 10... ou P1, P2, P10, V1, V101
lista.sort((a, b) =>
a.marca.localeCompare(b.marca, undefined, { numeric: true, sensitivity: 'base' })
);
setPecasStatus(lista);
} catch (e) {
console.error('Erro ao carregar dados de status:', e);
@@ -165,31 +217,92 @@ export const SmartConsultaRelatoriosFlow: React.FC<SmartConsultaRelatoriosFlowPr
carregarDadosCompletos();
}, [carregarDadosCompletos]);
// Filtragem da lista
// Lista de fases únicas para filtro
const fasesUnicas = useMemo(() => {
const setF = new Set<string>();
pecasStatus.forEach((p) => {
if (p.etapa_fase && p.etapa_fase.trim()) {
setF.add(p.etapa_fase.trim());
}
});
return Array.from(setF).sort((a, b) =>
a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' })
);
}, [pecasStatus]);
// Filtragem da lista refinada por Fase, Perfil Rápido, Texto e Status
const pecasFiltradas = useMemo(() => {
return pecasStatus.filter((p) => {
// Filtro texto
// 1. Filtro por Fase
if (filtroFase !== 'todas') {
if ((p.etapa_fase || '').trim() !== filtroFase) return false;
}
// 2. Filtro rápido de Perfil / Material (W, L, Chapa, Redondo, Tubo)
if (filtroPerfilRapido) {
const perfilNorm = (p.perfil_principal || '').toLowerCase();
const descNorm = (p.descricao || '').toLowerCase();
const matNorm = (p.material || '').toLowerCase();
const buscaP = filtroPerfilRapido.toLowerCase();
let bateuPerfil = false;
if (buscaP === 'w') {
bateuPerfil = perfilNorm.startsWith('w') || perfilNorm.includes('w150') || perfilNorm.includes('w200') || perfilNorm.includes('w250') || perfilNorm.includes('w310') || perfilNorm.includes('perfil w');
} else if (buscaP === 'l') {
bateuPerfil = perfilNorm.startsWith('l') || perfilNorm.includes('cant') || descNorm.includes('l2x');
} else if (buscaP === 'chapa') {
bateuPerfil = perfilNorm.includes('chapa') || perfilNorm.includes('ch.') || descNorm.includes('chapa') || descNorm.includes('arruela');
} else if (buscaP === 'redondo') {
bateuPerfil = perfilNorm.includes('red') || perfilNorm.includes('ferro') || perfilNorm.includes('barra') || perfilNorm.includes('ø') || descNorm.includes('chumb');
} else if (buscaP === 'tubo') {
bateuPerfil = perfilNorm.includes('tubo') || perfilNorm.includes('tubular') || descNorm.includes('tubo');
} else {
bateuPerfil = perfilNorm.includes(buscaP) || descNorm.includes(buscaP) || matNorm.includes(buscaP);
}
if (!bateuPerfil) return false;
}
// 3. Filtro texto (busca por marca, perfil, descrição ou material)
if (busca.trim()) {
const t = busca.toLowerCase();
const match = p.marca.toLowerCase().includes(t) || p.descricao?.toLowerCase().includes(t);
const t = busca.toLowerCase().trim();
const match =
p.marca.toLowerCase().includes(t) ||
(p.perfil_principal && p.perfil_principal.toLowerCase().includes(t)) ||
(p.descricao && p.descricao.toLowerCase().includes(t)) ||
(p.material && p.material.toLowerCase().includes(t)) ||
p.etapa_fase.toLowerCase().includes(t);
if (!match) return false;
}
// Filtro status
// 4. Filtro por status
if (filtroStatus === 'todos') return true;
if (filtroStatus === 'montado') return p.montado_obra >= p.quantidade;
if (filtroStatus === 'embarcado') return p.expedido >= p.quantidade;
if (filtroStatus === 'pronto') return p.pintura >= p.quantidade || p.solda >= p.quantidade;
if (filtroStatus === 'fabricando') return p.corte > 0 && p.pintura < p.quantidade;
if (filtroStatus === 'fabricando') return (p.corte > 0 || p.detalhamento > 0) && p.pintura < p.quantidade;
return true;
});
}, [pecasStatus, busca, filtroStatus]);
}, [pecasStatus, busca, filtroFase, filtroPerfilRapido, filtroStatus]);
// Status visual de cada etapa
// Limpar todos os filtros
const handleLimparFiltros = () => {
smartAudio.playClick();
setBusca('');
setFiltroFase('todas');
setFiltroPerfilRapido('');
setFiltroStatus('todos');
toast.info('Filtros restaurados');
};
const temFiltroAtivo =
busca !== '' || filtroFase !== 'todas' || filtroPerfilRapido !== '' || filtroStatus !== 'todos';
// Badge da etapa
const getBadgeEtapa = (concluido: number, total: number) => {
if (concluido >= total && total > 0) {
return { icon: <CheckCircle2 className="h-4 w-4 text-emerald-400" />, cor: 'text-emerald-400', label: 'OK' };
return { icon: <CheckCircle2 className="h-4 w-4 text-emerald-400" />, cor: 'text-emerald-400', label: `${concluido}` };
}
if (concluido > 0) {
return { icon: <Clock className="h-4 w-4 text-amber-400" />, cor: 'text-amber-400', label: `${concluido}/${total}` };
@@ -198,16 +311,15 @@ export const SmartConsultaRelatoriosFlow: React.FC<SmartConsultaRelatoriosFlowPr
};
// ─────────────────────────────────────────────────────────────
// GERAÇÃO DE RELATÓRIOS PDF (jsPDF)
// GERAÇÃO DE RELATÓRIOS EM PDF (jsPDF)
// ─────────────────────────────────────────────────────────────
// 1. Relatório do Turno de Hoje do Operador
// 1. Relatório do Turno de Hoje
const gerarRelatorioTurnoPDF = async (acao: 'download' | 'whatsapp' | 'email') => {
try {
setGerandoPDF(true);
const hoje = new Date().toISOString().split('T')[0];
// Buscar apontamentos feitos hoje pelo operador autenticado
const { data: meusApontamentos, error } = await supabase
.from('apontamentos_producao')
.select(`
@@ -235,10 +347,9 @@ export const SmartConsultaRelatoriosFlow: React.FC<SmartConsultaRelatoriosFlowPr
const dataFormatada = new Date().toLocaleDateString('pt-BR');
const horaFormatada = new Date().toLocaleTimeString('pt-BR');
// Cabeçalho
doc.setFillColor(15, 23, 42); // Slate-900
doc.setFillColor(15, 23, 42);
doc.rect(0, 0, 210, 30, 'F');
doc.setTextColor(251, 191, 36); // Amber-400
doc.setTextColor(251, 191, 36);
doc.setFontSize(16);
doc.setFont('helvetica', 'bold');
doc.text(titulo, 14, 15);
@@ -272,7 +383,6 @@ export const SmartConsultaRelatoriosFlow: React.FC<SmartConsultaRelatoriosFlowPr
];
});
// Tabela de Apontamentos
autoTable(doc, {
startY: 38,
head: [['Marca', 'Fase', 'Processo', 'Qtd', 'Peso Unit.', 'Peso Total']],
@@ -282,7 +392,6 @@ export const SmartConsultaRelatoriosFlow: React.FC<SmartConsultaRelatoriosFlowPr
styles: { fontSize: 9 },
});
// Rodapé Resumo
const finalY = (doc as any).lastAutoTable?.finalY || 80;
doc.setFillColor(241, 245, 249);
doc.rect(14, finalY + 5, 182, 20, 'F');
@@ -292,7 +401,6 @@ export const SmartConsultaRelatoriosFlow: React.FC<SmartConsultaRelatoriosFlowPr
doc.text(`RESUMO DO TURNO:`, 20, finalY + 14);
doc.text(`Total de Peças: ${totalPecas} un | Peso Total: ${totalKg.toFixed(1)} kg`, 20, finalY + 20);
// Tratamento da Ação
const nomeArquivo = `Turno_${obra.of_number}_${hoje}.pdf`;
if (acao === 'download') {
@@ -325,7 +433,7 @@ export const SmartConsultaRelatoriosFlow: React.FC<SmartConsultaRelatoriosFlowPr
} catch (e: any) {
smartAudio.playAlert();
console.error('Erro ao gerar relatório:', e);
toast.error('Erro ao gerar relatório do turno: ' + (e?.message || ''));
toast.error('Erro ao gerar relatório: ' + (e?.message || ''));
} finally {
setGerandoPDF(false);
}
@@ -351,8 +459,10 @@ export const SmartConsultaRelatoriosFlow: React.FC<SmartConsultaRelatoriosFlowPr
const tableRows = pecasStatus.map((p) => [
p.marca,
p.etapa_fase || 'Geral',
p.perfil_principal || p.descricao,
p.etapa_fase || '1',
p.quantidade.toString(),
p.detalhamento.toString(),
p.corte.toString(),
p.montagem.toString(),
p.solda.toString(),
@@ -363,7 +473,7 @@ export const SmartConsultaRelatoriosFlow: React.FC<SmartConsultaRelatoriosFlowPr
autoTable(doc, {
startY: 38,
head: [['Marca', 'Fase', 'Total', 'Corte', 'Mont.', 'Solda', 'Pint.', 'Exped.', 'Obra']],
head: [['Marca', 'Perfil', 'Fase', 'Total', 'Det.', 'Corte', 'Mont.', 'Solda', 'Pint.', 'Exp.', 'Obra']],
body: tableRows,
theme: 'grid',
headStyles: { fillColor: [30, 41, 59], textColor: [251, 191, 36], fontStyle: 'bold' },
@@ -382,9 +492,9 @@ export const SmartConsultaRelatoriosFlow: React.FC<SmartConsultaRelatoriosFlowPr
};
return (
<div className="flex flex-col flex-1 p-4 max-w-xl mx-auto w-full">
<div className="flex flex-col flex-1 p-3.5 max-w-xl mx-auto w-full">
{/* Navegação entre Abas: Consulta Rápida vs Relatórios */}
<div className="grid grid-cols-2 gap-2 mb-4 bg-slate-800 p-1.5 rounded-2xl border border-slate-700">
<div className="grid grid-cols-2 gap-2 mb-3 bg-slate-800 p-1 rounded-2xl border border-slate-700">
<button
type="button"
onClick={() => {
@@ -419,12 +529,12 @@ export const SmartConsultaRelatoriosFlow: React.FC<SmartConsultaRelatoriosFlowPr
</div>
{/* ─────────────────────────────────────────────────────────────
ABA 1: CONSULTA RÁPIDA DE PEÇAS COM TIMELINE
ABA 1: CONSULTA RÁPIDA DE PEÇAS COM FILTROS AVANÇADOS E TIMELINE
───────────────────────────────────────────────────────────── */}
{abaAtiva === 'consulta' && (
<div className="flex flex-col flex-1 overflow-hidden">
{/* Campo de Busca Gigante Touch */}
<div className="relative mb-3">
{/* Campo de Busca Rápida */}
<div className="relative mb-2">
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-slate-400">
<Search className="h-5 w-5 text-amber-400" />
</div>
@@ -432,87 +542,216 @@ export const SmartConsultaRelatoriosFlow: React.FC<SmartConsultaRelatoriosFlowPr
type="text"
value={busca}
onChange={(e) => setBusca(e.target.value)}
placeholder="Digite a marca (ex: V101, P205)..."
className="h-14 pl-11 pr-11 bg-slate-800/90 border-slate-700 text-slate-100 text-base font-medium rounded-2xl"
placeholder="Buscar por Marca, Perfil (W, L...), Descrição..."
className="h-13 pl-11 pr-11 bg-slate-800/90 border-slate-700 text-slate-100 text-sm sm:text-base font-medium rounded-2xl placeholder:text-slate-500"
/>
{busca && (
<button
type="button"
onClick={() => setBusca('')}
className="absolute inset-y-0 right-0 pr-3.5 flex items-center text-slate-400"
className="absolute inset-y-0 right-0 pr-3.5 flex items-center text-slate-400 hover:text-slate-200"
>
<X className="h-5 w-5" />
</button>
)}
</div>
{/* Pílulas de Filtro Rápido */}
<div className="flex items-center gap-1.5 overflow-x-auto pb-2 mb-2 no-scrollbar">
{/* FILTRO 1: Pílulas Rápidas de Perfis / Materiais Comuns (W, L, Chapa, Redondo, Tubo) */}
<div className="flex items-center gap-1.5 overflow-x-auto pb-1.5 mb-1.5 no-scrollbar">
<span className="text-[10px] font-black uppercase text-amber-400 px-1 shrink-0">
PERFIL:
</span>
{[
{ id: 'todos', label: 'Todos' },
{ id: 'fabricando', label: 'Fabricando' },
{ id: 'pronto', label: 'Pronto Pátio' },
{ id: 'embarcado', label: 'Embarcado' },
{ id: 'montado', label: 'Montado' },
].map((f) => (
{ id: 'w', label: 'W / I' },
{ id: 'l', label: 'L (Cantoneira)' },
{ id: 'chapa', label: 'Chapa' },
{ id: 'redondo', label: 'Barra / Redondo' },
{ id: 'tubo', label: 'Tubo' },
].map((p) => {
const ativo = filtroPerfilRapido === p.id;
return (
<button
key={p.id}
type="button"
onClick={() => {
smartAudio.playClick();
setFiltroPerfilRapido(ativo ? '' : p.id);
}}
className={`h-8 px-2.5 rounded-lg text-xs font-black whitespace-nowrap transition-all border ${
ativo
? 'bg-amber-500 text-slate-950 border-amber-400 shadow-sm'
: 'bg-slate-800 text-slate-300 border-slate-700 hover:border-slate-600'
}`}
>
{p.label}
</button>
);
})}
</div>
{/* FILTRO 2: Fases da Obra */}
<div className="flex items-center gap-1.5 overflow-x-auto pb-1.5 mb-1.5 no-scrollbar">
<span className="text-[10px] font-black uppercase text-blue-400 px-1 shrink-0">
FASE:
</span>
<button
type="button"
onClick={() => {
smartAudio.playClick();
setFiltroFase('todas');
}}
className={`h-8 px-2.5 rounded-lg text-xs font-bold whitespace-nowrap transition-all border ${
filtroFase === 'todas'
? 'bg-blue-600 text-white border-blue-400 font-black'
: 'bg-slate-800 text-slate-300 border-slate-700'
}`}
>
Todas
</button>
{fasesUnicas.map((fase) => (
<button
key={f.id}
key={fase}
type="button"
onClick={() => {
smartAudio.playClick();
setFiltroStatus(f.id as any);
setFiltroFase(fase);
}}
className={`h-9 px-3 rounded-xl font-bold text-xs whitespace-nowrap transition-all border ${
filtroStatus === f.id
? 'bg-amber-500 text-slate-950 border-amber-400'
className={`h-8 px-2.5 rounded-lg text-xs font-bold whitespace-nowrap transition-all border ${
filtroFase === fase
? 'bg-blue-600 text-white border-blue-400 font-black'
: 'bg-slate-800 text-slate-300 border-slate-700'
}`}
>
{f.label}
Fase {fase}
</button>
))}
</div>
{/* Lista de Peças com Linha do Tempo Visual */}
<div className="flex-1 overflow-y-auto space-y-3 pb-4">
{/* FILTRO 3: Status Fabril e Botão Limpar */}
<div className="flex items-center justify-between gap-1.5 pb-2 mb-2 border-b border-slate-800">
<div className="flex items-center gap-1.5 overflow-x-auto no-scrollbar">
{[
{ id: 'todos', label: 'Todos' },
{ id: 'fabricando', label: 'Em Fabricação' },
{ id: 'pronto', label: 'Pronto Pátio' },
{ id: 'embarcado', label: 'Embarcado' },
{ id: 'montado', label: 'Montado' },
].map((f) => (
<button
key={f.id}
type="button"
onClick={() => {
smartAudio.playClick();
setFiltroStatus(f.id as any);
}}
className={`h-7 px-2 rounded-md text-[11px] font-bold whitespace-nowrap transition-all ${
filtroStatus === f.id
? 'bg-slate-200 text-slate-900 font-black'
: 'bg-slate-800/80 text-slate-400 hover:text-slate-200'
}`}
>
{f.label}
</button>
))}
</div>
{temFiltroAtivo && (
<button
type="button"
onClick={handleLimparFiltros}
className="h-7 px-2.5 rounded-lg bg-red-950/50 hover:bg-red-900/60 border border-red-800/60 text-red-300 text-[11px] font-bold flex items-center gap-1 shrink-0 active:scale-95"
title="Limpar todos os filtros aplicados"
>
<RotateCcw className="h-3 w-3" />
<span>Limpar</span>
</button>
)}
</div>
{/* Contador de Peças Encontradas */}
<div className="flex items-center justify-between text-xs text-slate-400 px-1 mb-2">
<span>
Exibindo <b className="text-amber-400">{pecasFiltradas.length}</b> de{' '}
{pecasStatus.length} marcas
</span>
<span className="text-[11px] font-semibold text-slate-500">
Ordem Numérica Natural
</span>
</div>
{/* Lista de Peças Ordenadas com Timeline Completa */}
<div className="flex-1 overflow-y-auto space-y-2.5 pb-4">
{loading ? (
<div className="py-12 flex flex-col items-center justify-center text-slate-400 gap-2">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-amber-500 border-t-transparent" />
<span>Carregando timeline das peças...</span>
<span>Carregando peças e apontamentos...</span>
</div>
) : pecasFiltradas.length === 0 ? (
<div className="p-8 text-center bg-slate-800/40 rounded-2xl border border-slate-800">
<AlertCircle className="h-10 w-10 text-slate-500 mx-auto mb-2" />
<h3 className="text-base font-bold text-slate-200">Nenhuma peça encontrada</h3>
<p className="text-xs text-slate-400 mt-1">Verifique o termo de busca ou filtro</p>
<p className="text-xs text-slate-400 mt-1 mb-3">
Tente ajustar a busca, a fase ou clique em limpar filtros
</p>
<Button
type="button"
variant="outline"
onClick={handleLimparFiltros}
className="h-10 px-4 bg-slate-800 border-slate-700 text-amber-400 font-bold text-xs rounded-xl"
>
<RotateCcw className="h-3.5 w-3.5 mr-1.5" />
Limpar Filtros
</Button>
</div>
) : (
pecasFiltradas.map((peca) => (
<div
key={peca.id}
className="p-4 rounded-2xl bg-slate-800/95 border border-slate-700 shadow-sm"
className="p-3.5 rounded-2xl bg-slate-800/95 border border-slate-700 shadow-sm"
>
<div className="flex items-center justify-between">
<div>
<span className="text-xl font-black text-amber-400 tracking-wider">
{peca.marca}
</span>
<span className="ml-2 text-xs px-2 py-0.5 rounded bg-slate-700 text-slate-300 font-semibold">
{peca.etapa_fase || 'Estrutural'}
{/* Linha Superior: Marca, Perfil, Fase e Quantidade */}
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-xl font-black text-amber-400 tracking-wider">
{peca.marca}
</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}
</span>
)}
<span className="text-[11px] px-1.5 py-0.5 rounded bg-blue-500/15 text-blue-300 font-bold border border-blue-500/30">
Fase {peca.etapa_fase}
</span>
{peca.material && (
<span className="text-[10px] px-1.5 py-0.5 rounded bg-slate-700 text-slate-300 font-semibold">
{peca.material}
</span>
)}
</div>
<div className="text-xs text-slate-300 mt-1 line-clamp-1">
{peca.descricao || 'Peça Estrutural'}
</div>
</div>
<div className="text-right shrink-0">
<span className="text-xs text-slate-400 block font-medium">Total</span>
<span className="text-base font-black text-white">
{peca.quantidade}{' '}
<span className="text-xs font-normal text-slate-400">un</span>
</span>
</div>
<span className="text-xs font-bold text-slate-300">
Total: <span className="font-black text-white">{peca.quantidade} un</span>
</span>
</div>
<div className="text-xs text-slate-400 mt-0.5 truncate">
{peca.descricao || 'Peça Estrutural'}
</div>
{/* Linha do Tempo Visual da Peça */}
<div className="grid grid-cols-6 gap-1 mt-3.5 pt-3 border-t border-slate-700/80 text-center">
{/* 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 },
@@ -523,10 +762,10 @@ export const SmartConsultaRelatoriosFlow: React.FC<SmartConsultaRelatoriosFlowPr
const badge = getBadgeEtapa(etapa.valor, peca.quantidade);
return (
<div key={idx} className="flex flex-col items-center">
<div className="text-[10px] font-bold text-slate-400 mb-1">
<div className="text-[9px] font-bold text-slate-400 mb-0.5 uppercase">
{etapa.nome}
</div>
<div className="p-1 rounded-lg bg-slate-900 border border-slate-700/60 mb-1">
<div className="p-0.5 rounded-md bg-slate-900 border border-slate-700/60 mb-0.5">
{badge.icon}
</div>
<span className={`text-[10px] font-black ${badge.cor}`}>
@@ -649,7 +888,7 @@ export const SmartConsultaRelatoriosFlow: React.FC<SmartConsultaRelatoriosFlowPr
smartAudio.playClick();
onVoltarHub();
}}
className="w-full h-14 bg-slate-800 border-slate-700 text-slate-200 text-sm font-bold rounded-2xl mt-2 active:scale-98"
className="w-full h-13 bg-slate-800 border-slate-700 text-slate-200 text-sm font-bold rounded-2xl mt-2 active:scale-98"
>
⬅️ Voltar ao Menu Principal
</Button>
@@ -149,7 +149,7 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
// Colocar primeiro as que têm saldo pendente
if (a.saldo > 0 && b.saldo === 0) return -1;
if (a.saldo === 0 && b.saldo > 0) return 1;
return a.marca.localeCompare(b.marca);
return a.marca.localeCompare(b.marca, undefined, { numeric: true, sensitivity: 'base' });
});
}, [pecas, faseSelecionada, processoSelecionado, mapaProducao]);