diff --git a/src/pages/smart/components/SmartEmbarqueFlow.tsx b/src/pages/smart/components/SmartEmbarqueFlow.tsx index b951148..65b52d7 100644 --- a/src/pages/smart/components/SmartEmbarqueFlow.tsx +++ b/src/pages/smart/components/SmartEmbarqueFlow.tsx @@ -8,8 +8,18 @@ import { Package, User, FileCheck, - AlertCircle + AlertCircle, + Eye, + Download, + Share2, + Trash2, + FileText, + CheckCircle2, + Calendar, + Weight } from 'lucide-react'; +import jsPDF from 'jspdf'; +import autoTable from 'jspdf-autotable'; import { OFAtiva } from '@/hooks/useOFsAtivas'; import { useAuth } from '@/hooks/useAuth'; import { supabase } from '@/integrations/supabase/client'; @@ -28,9 +38,24 @@ interface RomaneioAberto { numero_romaneio: string; nome_motorista?: string; tipo_transporte?: string; + frete_tipo?: string; peso_total_romaneio?: number; status?: string; created_at?: string; + data_romaneio?: string; +} + +interface ItemRomaneioCarregado { + id: string; + romaneio_id: string; + peca_id: string; + marca: string; + descricao?: string; + fase?: string; + quantidade_expedida: number; + peso_unitario: number; + peso_total: number; + created_at?: string; } interface PecaPronta { @@ -44,22 +69,37 @@ interface PecaPronta { saldoDisponivel: number; } +type SubTelaEmbarque = + | 'menu' + | 'novo_romaneio' + | 'selecionar_romaneio' + | 'carregando_pecas' + | 'conferencia_romaneio'; + export const SmartEmbarqueFlow: React.FC = ({ obra, onVoltarHub }) => { const { user } = useAuth(); - // Estados: 'menu' | 'novo_romaneio' | 'selecionar_romaneio' | 'carregando_pecas' - const [subTela, setSubTela] = useState<'menu' | 'novo_romaneio' | 'selecionar_romaneio' | 'carregando_pecas'>('menu'); + // Subtelas do fluxo de embarque + const [subTela, setSubTela] = useState('menu'); - // Romaneio ativo para carregamento + // Romaneio ativo para carregamento ou conferência const [romaneioAtivo, setRomaneioAtivo] = useState(null); // Lista de romaneios da OF const [romaneios, setRomaneios] = useState([]); const [loadingRomaneios, setLoadingRomaneios] = useState(false); + // Itens carregados no romaneio ativo + const [itensRomaneio, setItensRomaneio] = useState([]); + const [loadingItensRomaneio, setLoadingItensRomaneio] = useState(false); + const [removendoItemId, setRemovendoItemId] = useState(null); + const [gerandoPDF, setGerandoPDF] = useState(false); + // Formulário de Novo Romaneio Rápido const [nomeMotorista, setNomeMotorista] = useState(''); - const [tipoTransporte, setTipoTransporte] = useState<'caminhao_pequeno' | 'caminhao_trucado' | 'caminhao_munck' | 'carreta_12m'>('caminhao_trucado'); + const [tipoTransporte, setTipoTransporte] = useState< + 'caminhao_pequeno' | 'caminhao_trucado' | 'caminhao_munck' | 'carreta_12m' + >('caminhao_trucado'); const [freteTipo, setFreteTipo] = useState<'proprio' | 'terceiros'>('proprio'); const [criandoRomaneio, setCriandoRomaneio] = useState(false); @@ -72,6 +112,25 @@ export const SmartEmbarqueFlow: React.FC = ({ obra, onVo const [qtdEmbarcar, setQtdEmbarcar] = useState(1); const [salvandoItem, setSalvandoItem] = useState(false); + // Formatadores de Transporte e Frete + const formatTransporte = (tipo?: string) => { + const mapa: Record = { + carro: 'Carro', + utilitario: 'Utilitário', + caminhao_pequeno: 'Caminhão Pequeno (Toco)', + caminhao_trucado: 'Caminhão Trucado', + caminhao_munck: 'Caminhão Munck', + carreta_12m: 'Carreta 12m', + carreta_15m: 'Carreta 15m', + especial: 'Especial / Prancha', + }; + return (tipo && mapa[tipo]) || tipo || 'Caminhão Padrão'; + }; + + const formatFrete = (tipo?: string) => { + return tipo === 'proprio' ? 'Próprio' : 'Terceiros / Contratado'; + }; + // Carregar Romaneios Abertos da OF const carregarRomaneios = React.useCallback(async () => { try { @@ -125,6 +184,12 @@ export const SmartEmbarqueFlow: React.FC = ({ obra, onVo saldoDisponivel: saldo, }; }); + + // Ordenação natural das marcas para localização rápida + formatadas.sort((a, b) => + a.marca.localeCompare(b.marca, undefined, { numeric: true, sensitivity: 'base' }) + ); + setPecasProntas(formatadas); } } catch (e) { @@ -134,11 +199,45 @@ export const SmartEmbarqueFlow: React.FC = ({ obra, onVo } }, [obra.of_number]); + // Carregar itens colocados dentro de um romaneio específico + const carregarItensDoRomaneio = React.useCallback(async (romaneioId: string) => { + try { + setLoadingItensRomaneio(true); + const { data, error } = await supabase + .from('itens_romaneio_pecas') + .select('*') + .eq('romaneio_id', romaneioId) + .order('created_at', { ascending: true }); + + if (error) throw error; + + const lista = data || []; + // Ordenar marcas naturalmente + lista.sort((a, b) => + (a.marca || '').localeCompare(b.marca || '', undefined, { numeric: true, sensitivity: 'base' }) + ); + + setItensRomaneio(lista); + } catch (e) { + console.error('Erro ao buscar itens do romaneio:', e); + toast.error('Erro ao carregar itens do caminhão'); + } finally { + setLoadingItensRomaneio(false); + } + }, []); + useEffect(() => { carregarRomaneios(); carregarPecasParaEmbarque(); }, [carregarRomaneios, carregarPecasParaEmbarque]); + // Sincronizar itens quando o romaneio ativo mudar + useEffect(() => { + if (romaneioAtivo?.id) { + carregarItensDoRomaneio(romaneioAtivo.id); + } + }, [romaneioAtivo?.id, carregarItensDoRomaneio]); + // Criar Novo Romaneio const handleCriarRomaneio = async () => { if (!user) return; @@ -176,6 +275,7 @@ export const SmartEmbarqueFlow: React.FC = ({ obra, onVo smartAudio.playSuccess(); toast.success(`Romaneio ${numRomaneio} criado!`); setRomaneioAtivo(data); + setItensRomaneio([]); setSubTela('carregando_pecas'); carregarRomaneios(); } catch (e: any) { @@ -187,6 +287,14 @@ export const SmartEmbarqueFlow: React.FC = ({ obra, onVo } }; + // Abrir Tela de Conferência para qualquer romaneio + const handleAbrirConferencia = (r: RomaneioAberto) => { + smartAudio.playClick(); + setRomaneioAtivo(r); + carregarItensDoRomaneio(r.id); + setSubTela('conferencia_romaneio'); + }; + // Incluir Peça no Romaneio const handleIncluirPeca = async () => { if (!romaneioAtivo || !pecaEmbarque || qtdEmbarcar <= 0) return; @@ -220,10 +328,13 @@ export const SmartEmbarqueFlow: React.FC = ({ obra, onVo smartAudio.playSuccess(); toast.success(`Adicionado: ${qtdEmbarcar}x ${pecaEmbarque.marca} ao caminhão!`); - // Atualizar estado local + // Atualizar estado local e listas setRomaneioAtivo((prev) => (prev ? { ...prev, peso_total_romaneio: novoPesoRomaneio } : null)); setPecaEmbarque(null); - carregarPecasParaEmbarque(); + await Promise.all([ + carregarPecasParaEmbarque(), + carregarItensDoRomaneio(romaneioAtivo.id), + ]); } catch (e: any) { smartAudio.playAlert(); console.error('Erro ao adicionar peça ao romaneio:', e); @@ -233,6 +344,210 @@ export const SmartEmbarqueFlow: React.FC = ({ obra, onVo } }; + // Remover / Estornar Item do Romaneio + const handleRemoverItemRomaneio = async (item: ItemRomaneioCarregado) => { + if (!romaneioAtivo) return; + try { + setRemovendoItemId(item.id); + const { error } = await supabase + .from('itens_romaneio_pecas') + .delete() + .eq('id', item.id); + + if (error) throw error; + + // Recalcular peso total do romaneio + const novoPeso = Math.max(0, (romaneioAtivo.peso_total_romaneio || 0) - item.peso_total); + await supabase + .from('romaneios_expedicao') + .update({ peso_total_romaneio: novoPeso }) + .eq('id', romaneioAtivo.id); + + setRomaneioAtivo((prev) => (prev ? { ...prev, peso_total_romaneio: novoPeso } : null)); + smartAudio.playSuccess(); + toast.success(`Peça ${item.marca} removida do romaneio.`); + + // Atualizar dados + await Promise.all([ + carregarItensDoRomaneio(romaneioAtivo.id), + carregarPecasParaEmbarque(), + carregarRomaneios(), + ]); + } catch (e: any) { + smartAudio.playAlert(); + toast.error('Erro ao remover item: ' + (e?.message || '')); + } finally { + setRemovendoItemId(null); + } + }; + + // Finalizar / Concluir Romaneio Oficialmente + const handleConcluirRomaneioOficial = async (novoStatus: 'Confirmado' | 'Expedido' = 'Expedido') => { + if (!romaneioAtivo) return; + try { + const { error } = await supabase + .from('romaneios_expedicao') + .update({ status: novoStatus }) + .eq('id', romaneioAtivo.id); + + if (error) throw error; + + smartAudio.playSuccess(); + toast.success(`Romaneio ${romaneioAtivo.numero_romaneio} concluído com sucesso (${novoStatus})!`); + await carregarRomaneios(); + setRomaneioAtivo(null); + setSubTela('menu'); + } catch (e: any) { + smartAudio.playAlert(); + toast.error('Erro ao finalizar romaneio: ' + (e?.message || '')); + } + }; + + // ───────────────────────────────────────────────────────────── + // GERAÇÃO DE PDF OFICIAL DO ROMANEIO (jsPDF + autoTable) + // ───────────────────────────────────────────────────────────── + const gerarRomaneioPDF = (acao: 'download' | 'whatsapp') => { + if (!romaneioAtivo) return; + try { + setGerandoPDF(true); + const doc = new jsPDF(); + const dataFormatada = new Date().toLocaleDateString('pt-BR'); + const horaFormatada = new Date().toLocaleTimeString('pt-BR'); + + // Topo estilizado escuro e dourado industrial + doc.setFillColor(15, 23, 42); // slate-900 + doc.rect(0, 0, 210, 32, 'F'); + doc.setTextColor(251, 191, 36); // amber-400 + doc.setFontSize(16); + doc.setFont('helvetica', 'bold'); + doc.text(`ROMANEIO DE EXPEDIÇÃO & CARGA`, 14, 15); + + doc.setFontSize(9); + doc.setTextColor(255, 255, 255); + doc.text(`OF: ${obra.of_number} | Cliente: ${obra.cliente || 'Industrial'}`, 14, 22); + doc.text( + `Romaneio: ${romaneioAtivo.numero_romaneio} | Emissão: ${dataFormatada} às ${horaFormatada}`, + 14, + 28 + ); + + // Caixa de Informações de Transporte + doc.setFillColor(241, 245, 249); // slate-100 + doc.roundedRect(14, 36, 182, 22, 2, 2, 'F'); + doc.setTextColor(15, 23, 42); + doc.setFontSize(9); + doc.setFont('helvetica', 'bold'); + doc.text(`DADOS DO TRANSPORTE:`, 18, 43); + + doc.setFont('helvetica', 'normal'); + doc.text(`Motorista: ${romaneioAtivo.nome_motorista || 'A definir'}`, 18, 51); + doc.text(`Veículo: ${formatTransporte(romaneioAtivo.tipo_transporte)}`, 85, 51); + doc.text(`Frete: ${formatFrete(romaneioAtivo.frete_tipo)}`, 150, 51); + + // Tabela de Peças Carregadas + const tableRows = itensRomaneio.map((item, idx) => [ + (idx + 1).toString(), + item.marca, + item.fase || '1', + item.descricao || 'Peça Estrutural', + item.quantidade_expedida.toString(), + `${Number(item.peso_unitario || 0).toFixed(1)} kg`, + `${Number(item.peso_total || 0).toFixed(1)} kg`, + ]); + + const totalPecas = itensRomaneio.reduce( + (acc, it) => acc + Number(it.quantidade_expedida || 0), + 0 + ); + const totalKg = itensRomaneio.reduce( + (acc, it) => acc + Number(it.peso_total || 0), + 0 + ); + + autoTable(doc, { + startY: 62, + head: [['#', 'Marca', 'Fase', 'Descrição da Peça', 'Qtd', 'Peso Unit.', 'Peso Total']], + body: tableRows, + theme: 'striped', + headStyles: { fillColor: [30, 41, 59], textColor: [251, 191, 36], fontStyle: 'bold' }, + styles: { fontSize: 8.5 }, + columnStyles: { + 0: { cellWidth: 10 }, + 1: { cellWidth: 26, fontStyle: 'bold' }, + 2: { cellWidth: 18 }, + 3: { cellWidth: 65 }, + 4: { cellWidth: 15, halign: 'center' }, + 5: { cellWidth: 24, halign: 'right' }, + 6: { cellWidth: 24, halign: 'right', fontStyle: 'bold' }, + }, + }); + + const finalY = (doc as any).lastAutoTable?.finalY || 120; + + // Resumo e Totais + doc.setFillColor(241, 245, 249); + doc.rect(14, finalY + 4, 182, 16, 'F'); + doc.setTextColor(15, 23, 42); + doc.setFontSize(10); + doc.setFont('helvetica', 'bold'); + doc.text(`TOTAIS DO ROMANEIO:`, 20, finalY + 11); + doc.text( + `Marcas: ${itensRomaneio.length} | Total de Peças: ${totalPecas} un | Peso Total: ${totalKg.toFixed(1)} kg`, + 20, + finalY + 16 + ); + + // Campos de Assinatura + const posYAssinaturas = finalY + 34; + if (posYAssinaturas < 260) { + doc.setDrawColor(148, 163, 184); + doc.setLineWidth(0.5); + + // Assinatura Fábrica + doc.line(18, posYAssinaturas, 95, posYAssinaturas); + doc.setFontSize(8); + doc.setFont('helvetica', 'normal'); + doc.text('Expedido por (Fábrica / Almoxarifado)', 22, posYAssinaturas + 5); + + // Assinatura Motorista + doc.line(115, posYAssinaturas, 192, posYAssinaturas); + doc.text('Recebido por (Motorista / Transportador)', 120, posYAssinaturas + 5); + + // Assinatura Canteiro + const posYObra = posYAssinaturas + 16; + doc.line(65, posYObra, 145, posYObra); + doc.text('Recebido na Obra (Canteiro / Montagem)', 72, posYObra + 5); + } + + const nomeArquivo = `Romaneio_${romaneioAtivo.numero_romaneio}.pdf`; + + if (acao === 'download') { + doc.save(nomeArquivo); + smartAudio.playSuccess(); + toast.success('Romaneio em PDF gerado e baixado!'); + } else if (acao === 'whatsapp') { + smartAudio.playClick(); + const textoMsg = encodeURIComponent( + `*TRACKSTEEL - ROMANEIO DE EXPEDIÇÃO*\n` + + `*Romaneio:* ${romaneioAtivo.numero_romaneio}\n` + + `*Obra:* OF ${obra.of_number} - ${obra.cliente || ''}\n` + + `*Motorista:* ${romaneioAtivo.nome_motorista || 'A definir'}\n` + + `*Veículo:* ${formatTransporte(romaneioAtivo.tipo_transporte)}\n` + + `*Carga:* ${itensRomaneio.length} marcas, ${totalPecas} peças (${totalKg.toFixed(1)} kg)\n` + + `*Status:* ${romaneioAtivo.status || 'Expedido'}\n\n` + + `_Conferência gerada via Modo Smart TrackSteel._` + ); + window.open(`https://wa.me/?text=${textoMsg}`, '_blank'); + } + } catch (e: any) { + smartAudio.playAlert(); + console.error('Erro ao gerar PDF do romaneio:', e); + toast.error('Erro ao gerar PDF: ' + (e?.message || '')); + } finally { + setGerandoPDF(false); + } + }; + // ───────────────────────────────────────────────────────────── // RENDER: SUBTELA 'MENU' // ───────────────────────────────────────────────────────────── @@ -250,7 +565,7 @@ export const SmartEmbarqueFlow: React.FC = ({ obra, onVo Apontar Embarque & Romaneio

- Carregue caminhões com as peças prontas no pátio + Carregue caminhões com as peças prontas no pátio ou confira romaneios

@@ -280,7 +595,7 @@ export const SmartEmbarqueFlow: React.FC = ({ obra, onVo - {/* Opção 2: Romaneios em Aberto */} + {/* Opção 2: Romaneios em Aberto (Carga ou Conferência) */} ))} - {/* Pílulas Touch: Tipo de Frete */} + {/* Tipo de Frete */}
- {[ - { id: 'proprio', label: 'Frota Própria' }, - { id: 'terceiros', label: 'Terceirizado' }, - ].map((f) => ( - - ))} + +
-
+
+ + {/* Botões de Ação Direta no Card */} +
+ + + +
+
)) )}
@@ -515,7 +870,7 @@ export const SmartEmbarqueFlow: React.FC = ({ obra, onVo }} 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" > - ⬅️ Voltar + ⬅️ Voltar ao Menu ); @@ -527,7 +882,7 @@ export const SmartEmbarqueFlow: React.FC = ({ obra, onVo if (subTela === 'carregando_pecas' && romaneioAtivo) { return (
- {/* Banner do Romaneio Ativo */} + {/* Banner do Romaneio Ativo com Atalho para Conferência */}
@@ -543,19 +898,39 @@ export const SmartEmbarqueFlow: React.FC = ({ obra, onVo {(romaneioAtivo.peso_total_romaneio || 0).toLocaleString('pt-BR')} kg + {itensRomaneio.length > 0 && ( + + ({itensRomaneio.length} marcas na carga) + + )}
- +
+ + +
{/* Modal/Seção de Quantidade para o Item Selecionado */} @@ -566,7 +941,7 @@ export const SmartEmbarqueFlow: React.FC = ({ obra, onVo {pecaEmbarque.marca} - Saldo: {pecaEmbarque.saldoDisponivel} un + Saldo Disponível: {pecaEmbarque.saldoDisponivel} un
@@ -688,19 +1063,19 @@ export const SmartEmbarqueFlow: React.FC = ({ obra, onVo )} - {/* Botão Finalizador do Romaneio */} + {/* Botão Finalizador: abre a tela de conferência & PDF */}
+
+ + ); + } + + // ───────────────────────────────────────────────────────────── + // RENDER: SUBTELA 'CONFERENCIA_ROMANEIO' (CONFERÊNCIA TOUCH & PDF) + // ───────────────────────────────────────────────────────────── + if (subTela === 'conferencia_romaneio' && romaneioAtivo) { + const totalPecasCarregadas = itensRomaneio.reduce( + (sum, item) => sum + Number(item.quantidade_expedida || 0), + 0 + ); + const pesoTotalCarregado = itensRomaneio.reduce( + (sum, item) => sum + Number(item.peso_total || 0), + 0 + ); + + return ( +
+ {/* Cabeçalho da Conferência */} +
+ + CONFERÊNCIA DE EMBARQUE + +

+ {romaneioAtivo.numero_romaneio} +

+
+ + OF {obra.of_number} — {obra.cliente || 'Industrial'} + + + {romaneioAtivo.status || 'Em conferência'} + +
+
+ + {/* KPIs em Grid Rápido */} +
+
+ + Marcas + + + {itensRomaneio.length} + +
+
+ + Peças + + + {totalPecasCarregadas} un + +
+
+ + Peso Carga + + + {pesoTotalCarregado.toLocaleString('pt-BR', { maximumFractionDigits: 1 })} kg + +
+
+ + {/* Card de Dados do Transporte */} +
+
+ Motorista: + + {romaneioAtivo.nome_motorista || 'Não informado'} + +
+
+ Veículo: + + {formatTransporte(romaneioAtivo.tipo_transporte)} + +
+
+ Tipo de Frete: + + {formatFrete(romaneioAtivo.frete_tipo)} + +
+
+ + {/* Lista de Peças Colocadas no Romaneio para Conferência */} +
+
+ Peças Colocadas no Caminhão ({itensRomaneio.length}) + Toque na lixeira para estornar +
+ + {loadingItensRomaneio ? ( +
+
+ Carregando itens do romaneio... +
+ ) : itensRomaneio.length === 0 ? ( +
+ +

+ Nenhuma peça colocada neste romaneio ainda +

+

+ Adicione peças prontas no caminhão para gerar o romaneio +

+ +
+ ) : ( + itensRomaneio.map((item, idx) => ( +
+
+
+ #{idx + 1} +
+
+
+ + {item.marca} + + + {item.fase || '1'} + +
+ {item.descricao && ( +
+ {item.descricao} +
+ )} +
+ Peso Total: {item.peso_total.toFixed(1)} kg ({item.peso_unitario.toFixed(1)} kg/un) +
+
+
+ +
+
+ + {item.quantidade_expedida} + + un +
+ + {/* Botão de Excluir / Estornar Item */} + +
+
+ )) + )} +
+ + {/* Botões de Ação na Base: Imprimir PDF, Compartilhar e Concluir */} +
+ {/* Botão Principal: Imprimir / Baixar PDF Oficial */} + + + {/* Linha com WhatsApp e Adicionar Mais Peças */} +
+ + + +
+ + {/* Botão de Concluir Romaneio Oficialmente */} + + + {/* Voltar ao Menu */} +