1010 lines
43 KiB
TypeScript
1010 lines
43 KiB
TypeScript
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}
|
||
/>
|
||
</>
|
||
);
|
||
};
|