Files
tracksteel_app/src/components/expedicao/ItensRomaneioModal.tsx
T

1010 lines
43 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, useMemo } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Card, CardContent } from '@/components/ui/card';
import { Checkbox } from '@/components/ui/checkbox';
import { toast } from 'sonner';
import { Package, Plus, Wrench, Expand, AlertTriangle } from 'lucide-react';
import { RomaneioExpedicao } from '@/hooks/useRomaneios';
import { useProcessosFabricacao } from '@/hooks/useProcessosFabricacao';
import { usePecas } from '@/hooks/usePecas';
import { useMobileResponsive } from '@/hooks/useMobileResponsive';
import { supabase } from '@/integrations/supabase/client';
import { PecasRomaneioExpandido } from './PecasRomaneioExpandido';
interface ItensRomaneioModalProps {
romaneio: RomaneioExpedicao;
isOpen: boolean;
onClose: () => void;
}
interface PecaDisponivel {
id: string;
marca: string;
descricao: string;
perfil_principal?: string | null;
etapa_fase: string;
quantidade_total_peca: number;
quantidade_ja_expedida: number;
quantidade_disponivel: number;
peso_unitario: number;
comprimento?: number | null;
prioridade: string;
}
interface ItemRomaneioPeca {
id: string;
peca_id: string;
quantidade: number;
peso_unitario: number;
peso_total: number;
comprimento?: number | null;
marca: string;
descricao: string;
etapa_fase: string;
prioridade: string;
}
interface ItemRomaneioInsumo {
id: string;
nome: string;
quantidade: number;
unidade: string;
}
export const ItensRomaneioModal: React.FC<ItensRomaneioModalProps> = ({
romaneio,
isOpen,
onClose
}) => {
const { isMobile, isTablet } = useMobileResponsive();
const { data: processos } = useProcessosFabricacao();
const { pecas: todasPecas } = usePecas();
// Estados para filtros
const [processoSelecionado, setProcessoSelecionado] = useState('Pintura/Galv');
const [faseFilter, setFaseFilter] = useState('Todas as fases');
const [marcaFilter, setMarcaFilter] = useState('');
const [pecaDisponivel, setPecaDisponivel] = useState('');
const [quantidadePeca, setQuantidadePeca] = useState('');
// Estados para dados dinâmicos
const [pecasDisponiveis, setPecasDisponiveis] = useState<PecaDisponivel[]>([]);
const [fasesDisponiveis, setFasesDisponiveis] = useState<string[]>([]);
const [marcasDisponiveis, setMarcasDisponiveis] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
// Estados para itens do romaneio
const [itensPeca, setItensPeca] = useState<ItemRomaneioPeca[]>([]);
const [itensInsumo, setItensInsumo] = useState<ItemRomaneioInsumo[]>([]);
const [loadingItens, setLoadingItens] = useState(false);
// Estados para insumos
const [nomeInsumo, setNomeInsumo] = useState('');
const [quantidadeInsumo, setQuantidadeInsumo] = useState('');
const [unidadeInsumo, setUnidadeInsumo] = useState('');
// Estados para adicionar todas as peças
const [adicionarTodas, setAdicionarTodas] = useState(false);
// Novo estado para o modal expandido
const [showExpandedModal, setShowExpandedModal] = useState(false);
// Buscar peças disponíveis baseadas no processo selecionado
const buscarPecasDisponiveis = async (processo: string) => {
if (!romaneio.of_number) return;
setLoading(true);
try {
// Buscar peças que estão no processo selecionado e disponíveis para expedição
const { data: apontamentosData, error } = await supabase
.from('apontamentos_producao')
.select(`
peca_id,
componente_id,
tipo_apontamento,
quantidade_produzida,
processo_id,
processo:processos_fabricacao!inner!apontamentos_producao_processo_id_fkey(nome),
componente:componentes_peca!apontamentos_producao_componente_id_fkey(id, quantidade_por_peca),
peca:pecas!inner!apontamentos_producao_peca_id_fkey(
id,
marca,
descricao,
perfil_principal,
etapa_fase,
quantidade,
peso_unitario,
comprimento,
prioridade,
of_number,
tem_componentes
)
`)
.eq('peca.of_number', romaneio.of_number)
.eq('processo.nome', processo);
if (error) {
console.error('Erro ao buscar peças disponíveis:', error);
return;
}
// 2. Buscar quanto dessa OF já foi embalado em TODOS os romaneios da OF
const { data: expedidosData, error: expError } = await supabase
.from('itens_romaneio_pecas')
.select(`
peca_id,
quantidade_expedida,
romaneio:romaneios_expedicao!inner!itens_romaneio_pecas_romaneio_id_fkey(of_number)
`)
.eq('romaneio.of_number', romaneio.of_number);
const expedidosMap = new Map<string, number>();
if (!expError && expedidosData) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(expedidosData as any[]).forEach((item: any) => {
const pid = item.peca_id;
const qtd = Number(item.quantidade_expedida) || 0;
const current = expedidosMap.get(pid) || 0;
expedidosMap.set(pid, current + qtd);
});
}
// 3. Agrupar por peça principal (NUNCA somar unidades de subcomponentes na peça principal)
const pecasAgrupadas = new Map<
string,
{
peca: any;
qtdPecaDireta: number;
componentesCortados: Map<string, { cortado: number; porPeca: number }>;
}
>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(apontamentosData as any[])?.forEach((apontamento: any) => {
const pecaObj = apontamento.peca;
const pecaId = pecaObj?.id;
if (!pecaId) return;
if (!pecasAgrupadas.has(pecaId)) {
pecasAgrupadas.set(pecaId, {
peca: pecaObj,
qtdPecaDireta: 0,
componentesCortados: new Map()
});
}
const grupo = pecasAgrupadas.get(pecaId)!;
const quantidade = Number(apontamento.quantidade_produzida) || 0;
const isComponente =
apontamento.tipo_apontamento === 'componente' || Boolean(apontamento.componente_id);
if (isComponente) {
const compId = apontamento.componente_id || 'comp_default';
const porPeca = Math.max(1, Number(apontamento.componente?.quantidade_por_peca) || 1);
const atualComp = grupo.componentesCortados.get(compId)?.cortado || 0;
grupo.componentesCortados.set(compId, {
cortado: atualComp + quantidade,
porPeca
});
} else {
grupo.qtdPecaDireta += quantidade;
}
});
// 4. Calcular o saldo Real da peça principal (Produzido da Peça Principal - Expedido em Romaneios)
const pecasDisponiveisFinal: PecaDisponivel[] = [];
pecasAgrupadas.forEach((grupo, pecaId) => {
const pecaObj = grupo.peca;
const qtdTotalPeca = Math.max(1, Number(pecaObj.quantidade) || 1);
let qtdProduzidaPecaPrincipal = 0;
if (grupo.qtdPecaDireta > 0) {
// Apontamento direto da peça principal (ou kit sincronizado)
qtdProduzidaPecaPrincipal = Math.min(qtdTotalPeca, grupo.qtdPecaDireta);
} else if (grupo.componentesCortados.size > 0) {
// Caso haja apenas apontamentos de subcomponentes no processo (ex: Corte antes da soldagem),
// converte para a quantidade equivalente da peça principal (limitada à quantidade total da peça na OF)
let maxPecasEquivalentes = 0;
grupo.componentesCortados.forEach(({ cortado, porPeca }) => {
const equivalentes = Math.floor(cortado / porPeca);
if (equivalentes > maxPecasEquivalentes) {
maxPecasEquivalentes = equivalentes;
}
});
qtdProduzidaPecaPrincipal = Math.min(
qtdTotalPeca,
maxPecasEquivalentes > 0 ? maxPecasEquivalentes : qtdTotalPeca
);
}
const qtdExpedida = expedidosMap.get(pecaId) || 0;
const saldoReal = Math.min(
qtdTotalPeca - qtdExpedida,
qtdProduzidaPecaPrincipal - qtdExpedida
);
if (saldoReal > 0) {
pecasDisponiveisFinal.push({
id: pecaObj.id,
marca: pecaObj.marca,
descricao: pecaObj.descricao || '',
perfil_principal: pecaObj.perfil_principal || null,
etapa_fase: pecaObj.etapa_fase || '',
quantidade_total_peca: qtdTotalPeca,
quantidade_ja_expedida: qtdExpedida,
quantidade_disponivel: saldoReal,
peso_unitario: Number(pecaObj.peso_unitario) || 0,
comprimento: pecaObj.comprimento || null,
prioridade: pecaObj.prioridade || 'P4'
});
}
});
setPecasDisponiveis(pecasDisponiveisFinal);
// Extrair fases e marcas únicas
const fasesUnicas = [...new Set(pecasDisponiveisFinal.map(p => p.etapa_fase).filter(Boolean))];
const marcasUnicas = [...new Set(pecasDisponiveisFinal.map(p => p.marca).filter(Boolean))];
setFasesDisponiveis(fasesUnicas);
setMarcasDisponiveis(marcasUnicas);
} catch (error) {
console.error('Erro ao buscar peças disponíveis:', error);
toast.error('Erro ao carregar peças disponíveis');
} finally {
setLoading(false);
}
};
// Carregar itens existentes do romaneio
const carregarItensRomaneio = async () => {
setLoadingItens(true);
try {
// Carregar peças do romaneio - usando a tabela correta
const { data: pecasData, error: pecasError } = await supabase
.from('itens_romaneio_pecas')
.select(`
*,
pecas!inner(marca, descricao, etapa_fase, peso_unitario, comprimento, prioridade)
`)
.eq('romaneio_id', romaneio.id);
if (pecasError) {
console.error('Erro ao carregar peças do romaneio:', pecasError);
} else if (pecasData) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const itensPecaFormatados = pecasData.map((item: any) => ({
id: item.id,
peca_id: item.peca_id,
quantidade: item.quantidade_expedida,
peso_unitario: item.peso_unitario,
peso_total: item.peso_total,
comprimento: item.pecas?.comprimento || item.comprimento || null,
marca: item.marca,
descricao: item.descricao || '',
etapa_fase: item.fase || '',
prioridade: 'P4'
}));
setItensPeca(itensPecaFormatados);
}
// Carregar insumos do romaneio - usando a tabela correta
const { data: insumosData, error: insumosError } = await supabase
.from('itens_romaneio_insumos')
.select('*')
.eq('romaneio_id', romaneio.id);
if (insumosError) {
console.error('Erro ao carregar insumos do romaneio:', insumosError);
} else if (insumosData) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const itensInsumoFormatados = insumosData.map((item: any) => ({
id: item.id,
nome: item.descricao,
quantidade: item.quantidade_expedida,
unidade: item.unidade
}));
setItensInsumo(itensInsumoFormatados);
}
} catch (error) {
console.error('Erro ao carregar itens do romaneio:', error);
toast.error('Erro ao carregar itens do romaneio');
} finally {
setLoadingItens(false);
}
};
// Carregar peças quando processo selecionado mudar
useEffect(() => {
if (processoSelecionado && isOpen) {
buscarPecasDisponiveis(processoSelecionado);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [processoSelecionado, isOpen, romaneio.of_number]);
// Definir processo padrão quando abrir o modal
useEffect(() => {
if (isOpen) {
setProcessoSelecionado('Pintura/Galv');
setFaseFilter('Todas as fases');
setMarcaFilter('');
setPecaDisponivel('');
setQuantidadePeca('');
setAdicionarTodas(false);
carregarItensRomaneio();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen]);
// Filtrar peças baseado nos filtros selecionados
const pecasFiltradas = useMemo(() => {
let filtradas = [...pecasDisponiveis];
if (faseFilter !== 'Todas as fases') {
filtradas = filtradas.filter(p => p.etapa_fase === faseFilter);
}
if (marcaFilter.trim()) {
filtradas = filtradas.filter(p =>
p.marca.toLowerCase().includes(marcaFilter.toLowerCase())
);
}
return filtradas.sort((a, b) => {
const aNum = a.marca.match(/\d+/);
const bNum = b.marca.match(/\d+/);
if (aNum && bNum) {
return parseInt(aNum[0]) - parseInt(bNum[0]);
}
return a.marca.localeCompare(b.marca, undefined, { numeric: true, sensitivity: 'base' });
});
}, [pecasDisponiveis, faseFilter, marcaFilter]);
const handleAdicionarPeca = async () => {
const limiteVeiculo = romaneio.comprimento_maximo_veiculo || 12000;
if (adicionarTodas) {
// Verificar se há peças que excedem a capacidade do veículo
const pecasExcedentes = pecasFiltradas.filter(p => p.comprimento && p.comprimento > limiteVeiculo);
if (pecasExcedentes.length > 0) {
const listaMarcas = pecasExcedentes.map(p => `${p.marca} (${p.comprimento}mm)`).join(', ');
const confirmar = window.confirm(
`⚠️ ALERTA DE CAPACIDADE DO VEÍCULO:\n\n` +
`Atenção: ${pecasExcedentes.length} peça(s) excedem o limite de ${limiteVeiculo} mm (${(limiteVeiculo / 1000).toFixed(1)}m) do veículo:\n` +
`${listaMarcas}\n\n` +
`Essas peças são maiores que o espaço de carga do veículo selecionado.\n\n` +
`Deseja realmente incluí-las no romaneio?`
);
if (!confirmar) {
toast.warning('Operação cancelada para evitar carregar peças que não cabem no caminhão.');
return;
}
}
// Adicionar todas as peças filtradas com suas quantidades disponíveis
for (const peca of pecasFiltradas) {
try {
const { error } = await supabase
.from('itens_romaneio_pecas')
.insert({
romaneio_id: romaneio.id,
peca_id: peca.id,
quantidade_expedida: peca.quantidade_disponivel,
peso_unitario: peca.peso_unitario,
peso_total: peca.quantidade_disponivel * peca.peso_unitario,
comprimento: peca.comprimento || null,
marca: peca.marca,
descricao: peca.descricao,
fase: peca.etapa_fase
});
if (error) throw error;
} catch (error) {
console.error('Erro ao adicionar peça:', error);
toast.error(`Erro ao adicionar peça ${peca.marca}`);
}
}
toast.success('Todas as peças foram adicionadas ao romaneio');
} else {
// Adicionar peça individual
if (!pecaDisponivel || !quantidadePeca) {
toast.error('Selecione uma peça e informe a quantidade');
return;
}
const pecaSelecionada = pecasDisponiveis.find(p => p.id === pecaDisponivel);
if (!pecaSelecionada) {
toast.error('Peça não encontrada');
return;
}
// Validação de comprimento do veículo
if (pecaSelecionada.comprimento && pecaSelecionada.comprimento > limiteVeiculo) {
const confirmar = window.confirm(
`⚠️ ALERTA DE CAPACIDADE DO VEÍCULO:\n\n` +
`A peça "${pecaSelecionada.marca}" tem comprimento de ${pecaSelecionada.comprimento} mm (${(pecaSelecionada.comprimento / 1000).toFixed(1)}m), ` +
`que ULTRAPASSA o comprimento máximo suportado pelo veículo (${limiteVeiculo} mm / ${(limiteVeiculo / 1000).toFixed(1)}m)!\n\n` +
`Essa peça NÃO caberá na carroceria do transporte selecionado.\n\n` +
`Deseja realmente adicionar esta peça mesmo com essa advertência?`
);
if (!confirmar) {
toast.warning(`Inclusão da peça ${pecaSelecionada.marca} (${pecaSelecionada.comprimento}mm) cancelada por restrição dimensional.`);
return;
}
}
try {
const quantidade = parseInt(quantidadePeca);
if (quantidade > pecaSelecionada.quantidade_disponivel) {
toast.error(`Quantidade não autorizada! Saldo disponível é ${pecaSelecionada.quantidade_disponivel}`);
return;
}
const { error } = await supabase
.from('itens_romaneio_pecas')
.insert({
romaneio_id: romaneio.id,
peca_id: pecaDisponivel,
quantidade_expedida: quantidade,
peso_unitario: pecaSelecionada.peso_unitario,
peso_total: quantidade * pecaSelecionada.peso_unitario,
comprimento: pecaSelecionada.comprimento || null,
marca: pecaSelecionada.marca,
descricao: pecaSelecionada.descricao,
fase: pecaSelecionada.etapa_fase
});
if (error) throw error;
toast.success('Peça adicionada ao romaneio');
} catch (error) {
console.error('Erro ao adicionar peça:', error);
toast.error('Erro ao adicionar peça ao romaneio');
}
}
// Recarregar itens e limpar formulário
carregarItensRomaneio();
if (processoSelecionado) {
buscarPecasDisponiveis(processoSelecionado);
}
setPecaDisponivel('');
setQuantidadePeca('');
setAdicionarTodas(false);
};
const handleAdicionarInsumo = async () => {
if (!nomeInsumo || !quantidadeInsumo || !unidadeInsumo) {
toast.error('Preencha todos os campos do insumo');
return;
}
try {
const { error } = await supabase
.from('itens_romaneio_insumos')
.insert({
romaneio_id: romaneio.id,
descricao: nomeInsumo,
quantidade_expedida: parseFloat(quantidadeInsumo),
unidade: unidadeInsumo,
tipo_insumo: 'Manual'
});
if (error) throw error;
toast.success('Insumo adicionado ao romaneio');
setNomeInsumo('');
setQuantidadeInsumo('');
setUnidadeInsumo('');
carregarItensRomaneio();
} catch (error) {
console.error('Erro ao adicionar insumo:', error);
toast.error('Erro ao adicionar insumo ao romaneio');
}
};
const handleRemoverItemPeca = async (itemId: string) => {
try {
const { error } = await supabase
.from('itens_romaneio_pecas')
.delete()
.eq('id', itemId);
if (error) throw error;
toast.success('Item removido do romaneio');
carregarItensRomaneio();
if (processoSelecionado) {
buscarPecasDisponiveis(processoSelecionado);
}
} catch (error) {
console.error('Erro ao remover item:', error);
toast.error('Erro ao remover item do romaneio');
}
};
const handleRemoverItemInsumo = async (itemId: string) => {
try {
const { error } = await supabase
.from('itens_romaneio_insumos')
.delete()
.eq('id', itemId);
if (error) throw error;
toast.success('Insumo removido do romaneio');
carregarItensRomaneio();
} catch (error) {
console.error('Erro ao remover insumo:', error);
toast.error('Erro ao remover insumo do romaneio');
}
};
const getPrioridadeColor = (prioridade: string) => {
switch (prioridade) {
case 'P1': return 'bg-red-100 text-red-800';
case 'P2': return 'bg-orange-100 text-orange-800';
case 'P3': return 'bg-blue-100 text-blue-800';
default: return 'bg-gray-100 text-gray-800';
}
};
const qtdTotalPecas = itensPeca.reduce((total, item) => total + item.quantidade, 0);
const pesoTotalPecas = itensPeca.reduce((total, item) => total + item.peso_total, 0);
const maiorComprimentoPecas = itensPeca.reduce(
(max, item) => Math.max(max, Number(item.comprimento) || 0),
0
);
return (
<>
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className={`
${isMobile
? 'w-[95vw] max-w-none h-[95vh] m-2'
: isTablet
? 'w-[90vw] max-w-4xl max-h-[85vh]'
: 'max-w-6xl max-h-[90vh]'
}
overflow-hidden flex flex-col
`}>
<DialogHeader className="pb-3 shrink-0">
<DialogTitle className={`flex items-center gap-2 ${isMobile ? 'text-base' : 'text-lg'}`}>
<Package className={`${isMobile ? 'h-4 w-4' : 'h-5 w-5'}`} />
<span className="truncate">
Itens do Romaneio {romaneio.numero_romaneio} - OF: {romaneio.of_number}
</span>
</DialogTitle>
</DialogHeader>
<ScrollArea className="flex-1 overflow-auto">
<div className="px-1">
<Tabs defaultValue="pecas" className="flex flex-col h-full">
<TabsList className={`grid w-full grid-cols-2 ${isMobile ? 'h-10' : 'h-11'} mb-4`}>
<TabsTrigger value="pecas" className={isMobile ? 'text-sm' : 'text-base'}>
Peças
</TabsTrigger>
<TabsTrigger value="insumos" className={isMobile ? 'text-sm' : 'text-base'}>
Insumos
</TabsTrigger>
</TabsList>
<TabsContent value="pecas" className="flex-1 space-y-3">
{/* Seção Adicionar Peça */}
<Card className="shrink-0">
<CardContent className={`${isMobile ? 'p-3' : 'p-4'}`}>
<h3 className={`font-semibold mb-3 ${isMobile ? 'text-base' : 'text-lg'}`}>
Adicionar Peça
</h3>
{/* Filtro de Processo */}
<div className="grid grid-cols-1 gap-3 mb-3">
<div>
<Label className={`flex items-center gap-2 font-medium mb-2 ${isMobile ? 'text-sm' : 'text-sm'}`}>
<Wrench className="h-4 w-4" />
Processo * (Obrigatório)
</Label>
<Select
value={processoSelecionado}
onValueChange={(value) => {
setProcessoSelecionado(value);
setFaseFilter('Todas as fases');
setMarcaFilter('');
setPecaDisponivel('');
}}
>
<SelectTrigger className={isMobile ? 'h-10' : 'h-11'}>
<SelectValue placeholder="Selecione o processo" />
</SelectTrigger>
<SelectContent>
{processos?.map((processo) => (
<SelectItem key={processo.id} value={processo.nome}>
{processo.nome}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Filtros dinâmicos */}
<div className={`grid gap-3 mb-3 ${isMobile ? 'grid-cols-1' : 'grid-cols-1 md:grid-cols-2'}`}>
<div>
<Label className={`font-medium mb-2 block ${isMobile ? 'text-sm' : 'text-sm'}`}>
Filtrar por Fase
</Label>
<Select value={faseFilter} onValueChange={setFaseFilter}>
<SelectTrigger className={isMobile ? 'h-10' : 'h-11'}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="Todas as fases">Todas as fases</SelectItem>
{fasesDisponiveis.map((fase) => (
<SelectItem key={fase} value={fase}>
{fase}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className={`font-medium mb-2 block ${isMobile ? 'text-sm' : 'text-sm'}`}>
Filtrar por Marca
</Label>
<Input
value={marcaFilter}
onChange={(e) => setMarcaFilter(e.target.value)}
placeholder="Digite a marca da peça..."
className={isMobile ? 'h-10' : 'h-11'}
/>
</div>
</div>
{/* Opção para adicionar todas as peças */}
<div className="flex items-start space-x-2 mb-3">
<Checkbox
id="adicionar-todas"
checked={adicionarTodas}
onCheckedChange={(checked) => setAdicionarTodas(checked as boolean)}
className="mt-1 shrink-0"
/>
<Label
htmlFor="adicionar-todas"
className={`leading-relaxed ${isMobile ? 'text-sm' : 'text-sm'}`}
>
Adicionar todas as peças listadas com suas respectivas quantidades
</Label>
</div>
{!adicionarTodas && (
<>
{/* Seletor de peça disponível */}
<div className={`grid gap-3 mb-3 ${isMobile ? 'grid-cols-1' : 'grid-cols-1 md:grid-cols-2'}`}>
<div>
<Label className={`font-medium mb-2 block ${isMobile ? 'text-sm' : 'text-sm'}`}>
Peça Disponível
</Label>
<Select
value={pecaDisponivel}
onValueChange={(val) => {
setPecaDisponivel(val);
const pSel = pecasDisponiveis.find((p) => p.id === val);
if (pSel) {
setQuantidadePeca(String(pSel.quantidade_disponivel));
}
}}
>
<SelectTrigger className={isMobile ? 'h-10' : 'h-11'}>
<SelectValue placeholder="Selecionar peça" />
</SelectTrigger>
<SelectContent>
{loading ? (
<SelectItem value="loading" disabled>Carregando...</SelectItem>
) : (
<>
{pecasFiltradas.length === 0 ? (
<SelectItem value="empty" disabled>
Nenhuma peça disponível para este processo
</SelectItem>
) : (
pecasFiltradas.map((peca) => {
const limite = romaneio.comprimento_maximo_veiculo || 12000;
const naoCabe = Boolean(peca.comprimento && peca.comprimento > limite);
const descClean = (peca.descricao || '').trim();
const perfilClean = (peca.perfil_principal || '').trim();
const descricaoCompleta =
descClean && perfilClean && descClean.toLowerCase() !== perfilClean.toLowerCase()
? `${descClean} - ${perfilClean}`
: descClean || perfilClean || 'Peça Principal';
const faseFormatada = peca.etapa_fase
? peca.etapa_fase.toLowerCase().startsWith('fase')
? peca.etapa_fase
: `Fase ${peca.etapa_fase}`
: '';
const infoSaldo =
peca.quantidade_ja_expedida > 0
? `Disp: ${peca.quantidade_disponivel} de ${peca.quantidade_total_peca} un`
: `Qtd Disp: ${peca.quantidade_disponivel} un`;
return (
<SelectItem key={peca.id} value={peca.id}>
<div className="flex items-center justify-between gap-2 w-full">
<span className="truncate font-medium">
{peca.marca}
{faseFormatada ? ` [${faseFormatada}]` : ''} - {descricaoCompleta} ({infoSaldo})
</span>
{peca.comprimento != null && Number(peca.comprimento) > 0 && (
<span className={`text-[11px] font-mono shrink-0 ml-2 px-1.5 py-0.5 rounded ${
naoCabe ? 'bg-destructive/20 text-destructive font-bold' : 'text-muted-foreground bg-muted'
}`}>
{peca.comprimento}mm {naoCabe ? '⚠️ NÃO CABE' : ''}
</span>
)}
</div>
</SelectItem>
);
})
)}
</>
)}
</SelectContent>
</Select>
</div>
<div>
<Label className={`font-medium mb-2 block ${isMobile ? 'text-sm' : 'text-sm'}`}>
Quantidade
</Label>
<Input
type="number"
min="1"
max={pecasDisponiveis.find(p => p.id === pecaDisponivel)?.quantidade_disponivel || 1}
value={quantidadePeca}
onChange={(e) => setQuantidadePeca(e.target.value)}
placeholder="Quantidade"
className={isMobile ? 'h-10' : 'h-11'}
/>
</div>
</div>
{/* Alerta de dimensão se a peça exceder a capacidade do veículo */}
{(() => {
const pSel = pecasDisponiveis.find(p => p.id === pecaDisponivel);
const limite = romaneio.comprimento_maximo_veiculo || 12000;
if (pSel?.comprimento && pSel.comprimento > limite) {
return (
<div className="p-2.5 mb-3 rounded-md bg-destructive/15 border border-destructive/40 text-destructive text-xs space-y-1">
<div className="flex items-center gap-1.5 font-bold text-xs text-destructive">
<AlertTriangle className="h-4 w-4 shrink-0" />
ALERTA DE DIMENSÃO: Peça não cabe no veículo ({limite}mm)
</div>
<p>
A peça <strong>{pSel.marca}</strong> possui <strong>{pSel.comprimento} mm ({(pSel.comprimento / 1000).toFixed(1)}m)</strong> e ULTRAPASSA o comprimento máximo de carregamento deste veículo ({(limite / 1000).toFixed(1)}m).
</p>
</div>
);
}
return null;
})()}
</>
)}
<Button
onClick={handleAdicionarPeca}
disabled={(!pecaDisponivel && !adicionarTodas) || loading}
className={`w-full ${isMobile ? 'h-10 text-sm' : 'h-11'}`}
>
<Plus className="h-4 w-4 mr-2" />
{adicionarTodas ? 'Adicionar Todas as Peças' : 'Adicionar Peça'}
</Button>
</CardContent>
</Card>
{/* Seção de resumo das peças no romaneio */}
<Card className="shrink-0">
<CardContent className={`${isMobile ? 'p-3' : 'p-4'}`}>
<div className={`flex justify-between items-start mb-3 ${isMobile ? 'flex-col gap-2' : 'flex-row'}`}>
<h4 className={`font-semibold ${isMobile ? 'text-sm' : 'text-md'}`}>
Peças no Romaneio
</h4>
<div className={`flex gap-2 ${isMobile ? 'text-xs flex-wrap' : 'text-sm'}`}>
<Badge variant="outline" className={isMobile ? 'text-xs px-2 py-1' : ''}>
Qtd Total: {qtdTotalPecas}
</Badge>
<Badge variant="outline" className={isMobile ? 'text-xs px-2 py-1' : ''}>
Peso Total: {pesoTotalPecas.toFixed(2)} kg
</Badge>
<Badge variant="outline" className={isMobile ? 'text-xs px-2 py-1' : ''}>
Maior Comprimento: {maiorComprimentoPecas > 0 ? `${maiorComprimentoPecas} mm` : '-'}
</Badge>
</div>
</div>
<div className="flex justify-center">
<Button
onClick={() => setShowExpandedModal(true)}
className={`bg-red-600 hover:bg-red-700 text-white ${isMobile ? 'h-10 px-4 text-sm' : 'px-6 py-2'}`}
>
<Expand className="h-4 w-4 mr-2" />
Expandir
</Button>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="insumos" className="flex-1 space-y-3">
{/* Seção Adicionar Insumo */}
<Card className="shrink-0">
<CardContent className={`${isMobile ? 'p-3' : 'p-4'}`}>
<h3 className={`font-semibold mb-3 ${isMobile ? 'text-base' : 'text-lg'}`}>
Adicionar Insumo
</h3>
<div className={`grid gap-3 mb-3 ${isMobile ? 'grid-cols-1' : 'grid-cols-1 md:grid-cols-3'}`}>
<div>
<Label className={`font-medium mb-2 block ${isMobile ? 'text-sm' : 'text-sm'}`}>
Nome do Insumo
</Label>
<Input
value={nomeInsumo}
onChange={(e) => setNomeInsumo(e.target.value)}
placeholder="Nome do insumo"
className={isMobile ? 'h-10' : 'h-11'}
/>
</div>
<div>
<Label className={`font-medium mb-2 block ${isMobile ? 'text-sm' : 'text-sm'}`}>
Quantidade
</Label>
<Input
type="number"
min="0"
step="0.01"
value={quantidadeInsumo}
onChange={(e) => setQuantidadeInsumo(e.target.value)}
placeholder="Quantidade"
className={isMobile ? 'h-10' : 'h-11'}
/>
</div>
<div>
<Label className={`font-medium mb-2 block ${isMobile ? 'text-sm' : 'text-sm'}`}>
Unidade
</Label>
<Input
value={unidadeInsumo}
onChange={(e) => setUnidadeInsumo(e.target.value)}
placeholder="Ex: kg, m, unid"
className={isMobile ? 'h-10' : 'h-11'}
/>
</div>
</div>
<Button
onClick={handleAdicionarInsumo}
disabled={!nomeInsumo || !quantidadeInsumo || !unidadeInsumo}
className={`w-full ${isMobile ? 'h-10 text-sm' : 'h-11'}`}
>
<Plus className="h-4 w-4 mr-2" />
Adicionar Insumo
</Button>
</CardContent>
</Card>
{/* Lista de insumos no romaneio */}
<Card className="flex-1">
<CardContent className={`${isMobile ? 'p-3' : 'p-4'} h-full flex flex-col`}>
<h4 className={`font-semibold mb-3 ${isMobile ? 'text-sm' : 'text-md'}`}>
Insumos no Romaneio
</h4>
<ScrollArea className="flex-1 min-h-[200px]">
{loadingItens ? (
<div className="text-center py-8">
<div className={`${isMobile ? 'text-sm' : 'text-base'}`}>Carregando...</div>
</div>
) : itensInsumo.length === 0 ? (
<div className={`text-center py-8 text-gray-500 ${isMobile ? 'text-sm' : 'text-base'}`}>
Nenhum insumo adicionado
</div>
) : (
<div className="space-y-2 pr-2">
<div className={`grid gap-2 font-medium text-gray-600 border-b pb-2 ${
isMobile
? 'grid-cols-2 text-xs'
: 'grid-cols-4 text-xs'
}`}>
<div>Nome</div>
{!isMobile && <div>Quantidade</div>}
{!isMobile && <div>Unidade</div>}
<div className={isMobile ? 'text-center' : ''}>
{isMobile ? 'Qtd/Unid/Ações' : 'Ações'}
</div>
</div>
{itensInsumo.map((item) => (
<div
key={item.id}
className={`grid gap-2 border-b py-2 ${
isMobile
? 'grid-cols-2 text-sm'
: 'grid-cols-4 text-sm'
}`}
>
<div className="font-medium truncate">{item.nome}</div>
{isMobile ? (
<div className="text-right">
<div className="text-xs text-gray-600">
{item.quantidade} {item.unidade}
</div>
<Button
size="sm"
variant="destructive"
onClick={() => handleRemoverItemInsumo(item.id)}
className="mt-1 h-6 w-6 p-0"
>
×
</Button>
</div>
) : (
<>
<div className="text-center">{item.quantidade}</div>
<div>{item.unidade}</div>
<div className="text-center">
<Button
size="sm"
variant="destructive"
onClick={() => handleRemoverItemInsumo(item.id)}
className="h-7 w-7 p-0"
>
×
</Button>
</div>
</>
)}
</div>
))}
</div>
)}
</ScrollArea>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
</ScrollArea>
<div className={`flex justify-end pt-3 border-t shrink-0 ${isMobile ? 'px-1' : ''}`}>
<Button
variant="outline"
onClick={onClose}
className={isMobile ? 'h-10 px-4 text-sm' : ''}
>
Fechar
</Button>
</div>
</DialogContent>
</Dialog>
{/* Modal expandido para peças */}
<PecasRomaneioExpandido
isOpen={showExpandedModal}
onClose={() => setShowExpandedModal(false)}
romaneio={romaneio}
onRefresh={carregarItensRomaneio}
/>
</>
);
};