🚀 Initial commit: Versão atual do TrackSteel APP

This commit is contained in:
2026-03-18 21:17:53 +00:00
commit bde410c9ad
633 changed files with 108150 additions and 0 deletions
@@ -0,0 +1,134 @@
import React, { useEffect, useState } from 'react';
import { useApontamentoAutomaticoRomaneio, PecaComProcessosPulados } from '@/hooks/useApontamentoAutomaticoRomaneio';
import { ProcessosPuladosModal } from './ProcessosPuladosModal';
import { toast } from 'sonner';
interface ApontamentoAutomaticoHandlerProps {
romaneio: any;
onApontamentoConcluido: () => void;
}
export const ApontamentoAutomaticoHandler: React.FC<ApontamentoAutomaticoHandlerProps> = ({
romaneio,
onApontamentoConcluido
}) => {
const { processarApontamentos, loading } = useApontamentoAutomaticoRomaneio();
const [showProcessosPuladosModal, setShowProcessosPuladosModal] = useState(false);
const [pecasComProcessosPulados, setPecasComProcessosPulados] = useState<PecaComProcessosPulados[]>([]);
const [pecasOK, setPecasOK] = useState<any[]>([]);
useEffect(() => {
if (romaneio) {
iniciarProcessamento();
}
}, [romaneio]);
const iniciarProcessamento = async () => {
try {
console.log('🚀 Iniciando processamento automático do romaneio:', romaneio.numero_romaneio);
if (!romaneio.itens_pecas || romaneio.itens_pecas.length === 0) {
console.log('ℹ️ Romaneio sem peças para processar');
onApontamentoConcluido();
return;
}
const itensParaProcessar = romaneio.itens_pecas.map((item: any) => ({
peca_id: item.peca_id,
marca: item.marca,
quantidade_expedida: item.quantidade_expedida,
fase: item.fase
}));
console.log('📋 Itens para processar:', itensParaProcessar);
const resultado = await processarApontamentos.mutateAsync({
romaneioId: romaneio.id,
numeroRomaneio: romaneio.numero_romaneio,
ofNumber: romaneio.of_number,
itens: itensParaProcessar,
criarProcessosRetroativos: false
});
console.log('📊 Resultado do processamento:', resultado);
if (resultado.precisaConfirmacaoProcessosPulados) {
console.log('⚠️ Confirmação necessária para processos pulados');
setPecasComProcessosPulados(resultado.pecasComProcessosPulados || []);
setPecasOK(resultado.pecasOK || []);
setShowProcessosPuladosModal(true);
} else if (resultado.sucesso) {
console.log('✅ Processamento concluído com sucesso');
onApontamentoConcluido();
} else {
console.log('ℹ️ Processamento não realizado:', resultado.erro || 'Motivo não especificado');
onApontamentoConcluido();
}
} catch (error) {
console.error('❌ Erro no processamento automático:', error);
onApontamentoConcluido();
}
};
const handleConfirmarProcessosRetroativos = async () => {
try {
console.log('✅ Usuário confirmou criação de processos retroativos');
// Combinar peças OK com peças que têm processos pulados
const todosItens = [
...pecasOK,
...pecasComProcessosPulados.map(peca => ({
peca_id: peca.peca_id,
marca: peca.marca,
quantidade_expedida: peca.quantidade_expedida,
fase: peca.fase
}))
];
const resultado = await processarApontamentos.mutateAsync({
romaneioId: romaneio.id,
numeroRomaneio: romaneio.numero_romaneio,
ofNumber: romaneio.of_number,
itens: todosItens,
criarProcessosRetroativos: true
});
console.log('📊 Resultado com processos retroativos:', resultado);
setShowProcessosPuladosModal(false);
onApontamentoConcluido();
} catch (error) {
console.error('❌ Erro ao criar processos retroativos:', error);
setShowProcessosPuladosModal(false);
onApontamentoConcluido();
}
};
const handleCancelarApontamento = () => {
console.log('❌ Usuário cancelou o apontamento automático');
toast.info('Apontamento automático cancelado pelo usuário');
setShowProcessosPuladosModal(false);
onApontamentoConcluido();
};
const handleFecharModal = () => {
setShowProcessosPuladosModal(false);
onApontamentoConcluido();
};
return (
<>
{showProcessosPuladosModal && (
<ProcessosPuladosModal
isOpen={showProcessosPuladosModal}
onClose={handleFecharModal}
onConfirm={handleConfirmarProcessosRetroativos}
onCancel={handleCancelarApontamento}
pecasComProcessosPulados={pecasComProcessosPulados}
loading={loading}
/>
)}
</>
);
};
@@ -0,0 +1,81 @@
import React, { useEffect } from 'react';
import { ApontamentoAutomaticoHandler } from './ApontamentoAutomaticoHandler';
interface RomaneioEntregueDetail {
romaneioId: string;
status: string;
romaneioData?: any;
statusAnterior?: string;
}
export const ApontamentoAutomaticoListener: React.FC = () => {
const [romaneioParaProcessar, setRomaneioParaProcessar] = React.useState<any>(null);
useEffect(() => {
const handleRomaneioEntregue = async (event: CustomEvent<RomaneioEntregueDetail>) => {
console.log('🎯 Evento romaneio-entregue capturado:', event.detail);
const { romaneioId, romaneioData, status, statusAnterior } = event.detail;
try {
// Verificação adicional de segurança
if (status !== 'Entregue') {
console.log('❌ Status não é "Entregue", ignorando evento');
return;
}
if (statusAnterior === 'Entregue') {
console.log('❌ Status anterior já era "Entregue", ignorando evento');
return;
}
// Verificar se temos dados do romaneio
if (!romaneioData) {
console.log('⚠️ Dados do romaneio não fornecidos no evento');
return;
}
// Verificar se o romaneio tem peças para processar
if (!romaneioData.itens_pecas || romaneioData.itens_pecas.length === 0) {
console.log('ℹ️ Romaneio sem peças para apontar automaticamente');
return;
}
console.log('✅ Iniciando processamento do apontamento automático');
console.log('📦 Romaneio:', romaneioData.numero_romaneio);
console.log('🔧 OF:', romaneioData.of_number);
console.log('📋 Peças encontradas:', romaneioData.itens_pecas.length);
console.log('🔄 Mudança de status:', `"${statusAnterior}" → "${status}"`);
setRomaneioParaProcessar(romaneioData);
} catch (error) {
console.error('❌ Erro ao processar romaneio entregue:', error);
}
};
// Adicionar listener para o evento customizado
window.addEventListener('romaneio-entregue', handleRomaneioEntregue as EventListener);
// Cleanup
return () => {
window.removeEventListener('romaneio-entregue', handleRomaneioEntregue as EventListener);
};
}, []);
// Renderizar o handler de apontamento automático quando há um romaneio para processar
if (romaneioParaProcessar) {
return (
<ApontamentoAutomaticoHandler
romaneio={romaneioParaProcessar}
onApontamentoConcluido={() => {
console.log('✅ Apontamento automático concluído');
setRomaneioParaProcessar(null);
}}
/>
);
}
return null;
};
@@ -0,0 +1,127 @@
import React, { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Label } from '@/components/ui/label';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
import { AlertTriangle, Info } from 'lucide-react';
import { ConflitoPeca } from '@/hooks/useApontamentoAutomaticoRomaneio';
interface ConflitoApontamentoModalProps {
isOpen: boolean;
onClose: () => void;
conflitos: ConflitoPeca[];
onConfirmar: (resolucao: 'somar' | 'substituir' | 'cancelar') => void;
numeroRomaneio: string;
loading?: boolean;
}
export const ConflitoApontamentoModal = ({
isOpen,
onClose,
conflitos,
onConfirmar,
numeroRomaneio,
loading = false
}: ConflitoApontamentoModalProps) => {
const [resolucao, setResolucao] = useState<'somar' | 'substituir' | 'cancelar'>('somar');
const handleConfirmar = () => {
onConfirmar(resolucao);
};
const handleCancelar = () => {
onConfirmar('cancelar');
onClose();
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-yellow-600">
<AlertTriangle className="w-5 h-5" />
Conflito de Apontamentos - Romaneio {numeroRomaneio}
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<Alert>
<Info className="w-4 h-4" />
<AlertDescription>
Foram encontradas peças que já possuem apontamentos no processo "Montagem".
Como deseja proceder?
</AlertDescription>
</Alert>
{/* Lista de conflitos */}
<div className="space-y-3">
<h3 className="font-medium text-sm">Peças com conflitos:</h3>
{conflitos.map((conflito, index) => (
<div key={index} className="border rounded-lg p-3 bg-yellow-50">
<div className="flex items-center justify-between mb-2">
<div className="font-medium">{conflito.marca}</div>
{conflito.fase && (
<Badge variant="outline" className="text-xs">
{conflito.fase}
</Badge>
)}
</div>
<div className="text-sm text-muted-foreground">
<div>Quantidade já apontada: <span className="font-medium">{conflito.quantidade_existente}</span></div>
<div>Quantidade no romaneio: <span className="font-medium">{conflito.quantidade_romaneio}</span></div>
</div>
</div>
))}
</div>
{/* Opções de resolução */}
<div className="space-y-3">
<h3 className="font-medium text-sm">Como resolver os conflitos:</h3>
<RadioGroup value={resolucao} onValueChange={(value) => setResolucao(value as any)}>
<div className="flex items-center space-x-2">
<RadioGroupItem value="somar" id="somar" />
<Label htmlFor="somar" className="text-sm">
<div className="font-medium">Somar quantidades</div>
<div className="text-xs text-muted-foreground">
Adicionar as quantidades do romaneio às já existentes
</div>
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="substituir" id="substituir" />
<Label htmlFor="substituir" className="text-sm">
<div className="font-medium">Substituir apontamentos</div>
<div className="text-xs text-muted-foreground">
Remover apontamentos existentes e usar apenas as quantidades do romaneio
</div>
</Label>
</div>
</RadioGroup>
</div>
{/* Botões */}
<div className="flex justify-end gap-3 pt-4 border-t">
<Button
variant="outline"
onClick={handleCancelar}
disabled={loading}
>
Cancelar
</Button>
<Button
onClick={handleConfirmar}
disabled={loading}
className="bg-blue-600 hover:bg-blue-700"
>
{loading ? 'Processando...' : 'Confirmar'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
};
@@ -0,0 +1,789 @@
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 } 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;
etapa_fase: string;
quantidade_disponivel: number;
peso_unitario: number;
prioridade: string;
}
interface ItemRomaneioPeca {
id: string;
peca_id: string;
quantidade: number;
peso_unitario: number;
peso_total: number;
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,
quantidade_produzida,
processo_id,
processos_fabricacao!inner(nome),
pecas!inner(
id,
marca,
descricao,
etapa_fase,
peso_unitario,
prioridade,
of_number
)
`)
.eq('pecas.of_number', romaneio.of_number)
.eq('processos_fabricacao.nome', processo);
if (error) {
console.error('Erro ao buscar peças disponíveis:', error);
return;
}
// Agrupar por peça e calcular quantidade disponível
const pecasAgrupadas = new Map<string, PecaDisponivel>();
apontamentosData?.forEach(apontamento => {
const pecaId = apontamento.pecas.id;
const quantidade = apontamento.quantidade_produzida || 0;
if (pecasAgrupadas.has(pecaId)) {
const pecaExistente = pecasAgrupadas.get(pecaId)!;
pecaExistente.quantidade_disponivel += quantidade;
} else {
pecasAgrupadas.set(pecaId, {
id: apontamento.pecas.id,
marca: apontamento.pecas.marca,
descricao: apontamento.pecas.descricao || '',
etapa_fase: apontamento.pecas.etapa_fase || '',
quantidade_disponivel: quantidade,
peso_unitario: apontamento.pecas.peso_unitario || 0,
prioridade: apontamento.pecas.prioridade || 'P4'
});
}
});
const pecasArray = Array.from(pecasAgrupadas.values());
setPecasDisponiveis(pecasArray);
// Extrair fases e marcas únicas
const fasesUnicas = [...new Set(pecasArray.map(p => p.etapa_fase).filter(Boolean))];
const marcasUnicas = [...new Set(pecasArray.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, prioridade)
`)
.eq('romaneio_id', romaneio.id);
if (pecasError) {
console.error('Erro ao carregar peças do romaneio:', pecasError);
} else if (pecasData) {
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,
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) {
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);
}
}, [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();
}
}, [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);
});
}, [pecasDisponiveis, faseFilter, marcaFilter]);
const handleAdicionarPeca = async () => {
if (adicionarTodas) {
// 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,
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;
}
try {
const quantidade = parseInt(quantidadePeca);
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,
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();
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();
} 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);
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={setPecaDisponivel}>
<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) => (
<SelectItem key={peca.id} value={peca.id}>
<span className="truncate">
{peca.marca} - {peca.descricao} (Qtd: {peca.quantidade_disponivel})
</span>
</SelectItem>
))
)}
</>
)}
</SelectContent>
</Select>
</div>
<div>
<Label className={`font-medium mb-2 block ${isMobile ? 'text-sm' : 'text-sm'}`}>
Quantidade
</Label>
<Input
type="number"
min="1"
value={quantidadePeca}
onChange={(e) => setQuantidadePeca(e.target.value)}
placeholder="Quantidade"
className={isMobile ? 'h-10' : 'h-11'}
/>
</div>
</div>
</>
)}
<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>
</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}
/>
</>
);
};
@@ -0,0 +1,301 @@
import React, { useState, useEffect } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Edit2, Trash2, X } from 'lucide-react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
import { RomaneioExpedicao } from '@/hooks/useRomaneios';
import { useMobileResponsive } from '@/hooks/useMobileResponsive';
interface PecasRomaneioExpandidoProps {
isOpen: boolean;
onClose: () => void;
romaneio: RomaneioExpedicao;
onRefresh: () => void;
}
interface ItemRomaneioPeca {
id: string;
peca_id: string;
quantidade_expedida: number;
peso_unitario: number;
peso_total: number;
marca: string;
descricao: string;
fase: string;
prioridade: string;
}
export const PecasRomaneioExpandido: React.FC<PecasRomaneioExpandidoProps> = ({
isOpen,
onClose,
romaneio,
onRefresh
}) => {
const { isMobile, isTablet } = useMobileResponsive();
const [itensPeca, setItensPeca] = useState<ItemRomaneioPeca[]>([]);
const [loading, setLoading] = useState(false);
const carregarItensPecas = async () => {
setLoading(true);
try {
const { data, error } = await supabase
.from('itens_romaneio_pecas')
.select('*')
.eq('romaneio_id', romaneio.id)
.order('marca');
if (error) throw error;
if (data) {
const itensFormatados = data.map((item: any) => ({
id: item.id,
peca_id: item.peca_id,
quantidade_expedida: item.quantidade_expedida,
peso_unitario: item.peso_unitario,
peso_total: item.peso_total,
marca: item.marca,
descricao: item.descricao || '',
fase: item.fase || '',
prioridade: 'P4'
}));
setItensPeca(itensFormatados);
}
} catch (error) {
console.error('Erro ao carregar peças:', error);
toast.error('Erro ao carregar peças do romaneio');
} finally {
setLoading(false);
}
};
useEffect(() => {
if (isOpen) {
carregarItensPecas();
}
}, [isOpen, romaneio.id]);
const handleRemoverItem = 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');
carregarItensPecas();
onRefresh();
} catch (error) {
console.error('Erro ao remover item:', error);
toast.error('Erro ao remover item do romaneio');
}
};
const handleEditarItem = (item: ItemRomaneioPeca) => {
// Funcionalidade de edição pode ser implementada aqui
toast.info('Funcionalidade de edição em desenvolvimento');
};
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 qtdTotal = itensPeca.reduce((total, item) => total + item.quantidade_expedida, 0);
const pesoTotal = itensPeca.reduce((total, item) => total + item.peso_total, 0);
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className={`
${isMobile
? 'w-[95vw] max-w-none h-[95vh] m-2'
: isTablet
? 'w-[90vw] max-w-5xl h-[90vh]'
: 'max-w-6xl h-[95vh]'
}
bg-slate-900 text-white border-slate-700 overflow-hidden flex flex-col
`}>
<DialogHeader className="border-b border-slate-700 pb-3 shrink-0">
<div className="flex justify-between items-center">
<DialogTitle className={`font-bold text-white ${isMobile ? 'text-lg' : 'text-xl'}`}>
<span className="truncate">
Peças do Romaneio {romaneio.numero_romaneio} - OF: {romaneio.of_number}
</span>
</DialogTitle>
<Button
onClick={onClose}
variant="ghost"
size="sm"
className={`text-gray-400 hover:text-white p-0 ${isMobile ? 'h-6 w-6' : 'h-8 w-8'}`}
>
<X className={isMobile ? 'h-3 w-3' : 'h-4 w-4'} />
</Button>
</div>
<div className={`flex gap-2 mt-3 ${isMobile ? 'flex-wrap' : ''}`}>
<Badge variant="outline" className={`border-blue-500 text-blue-400 ${isMobile ? 'text-xs px-2 py-1' : ''}`}>
Qtd Total: {qtdTotal}
</Badge>
<Badge variant="outline" className={`border-green-500 text-green-400 ${isMobile ? 'text-xs px-2 py-1' : ''}`}>
Peso Total: {pesoTotal.toFixed(2)} kg
</Badge>
</div>
</DialogHeader>
<div className="flex-1 min-h-0">
{loading ? (
<div className="flex justify-center items-center h-64">
<div className={`text-gray-400 ${isMobile ? 'text-sm' : 'text-base'}`}>
Carregando...
</div>
</div>
) : itensPeca.length === 0 ? (
<div className="flex justify-center items-center h-64">
<div className={`text-gray-400 ${isMobile ? 'text-sm' : 'text-base'}`}>
Nenhuma peça encontrada no romaneio
</div>
</div>
) : (
<ScrollArea className="h-full w-full">
<div className="space-y-1 p-1 pb-4">
{/* Cabeçalho da tabela - Desktop/Tablet */}
{!isMobile && (
<div className={`grid gap-3 font-medium text-gray-400 bg-slate-800 p-3 rounded-t-md sticky top-0 z-10 ${
isTablet ? 'grid-cols-8 text-xs' : 'grid-cols-10 text-xs'
}`}>
<div>Marca</div>
<div>Fase</div>
<div className={isTablet ? 'col-span-1' : 'col-span-2'}>Descrição</div>
<div className="text-center">Qtd</div>
<div className="text-right">Peso Unit.</div>
<div className="text-right">Peso Total</div>
{!isTablet && <div className="text-center">Prioridade</div>}
<div className={`text-center ${isTablet ? 'col-span-2' : 'col-span-2'}`}>Ações</div>
</div>
)}
{/* Linhas de dados */}
{itensPeca.map((item, index) => (
<div key={item.id}>
{isMobile ? (
// Layout mobile - card style
<div className={`p-3 border border-slate-700 rounded-md mb-2 ${
index % 2 === 0 ? 'bg-slate-800/20' : 'bg-slate-800/40'
}`}>
<div className="flex justify-between items-start mb-2">
<div className="flex-1">
<div className="font-medium text-white text-sm">{item.marca}</div>
<div className="text-gray-300 text-xs">{item.fase}</div>
</div>
<div className="flex gap-1">
<Button
size="sm"
variant="ghost"
onClick={() => handleEditarItem(item)}
className="h-6 w-6 p-0 text-blue-400 hover:text-blue-300 hover:bg-blue-500/20"
>
<Edit2 className="h-3 w-3" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => handleRemoverItem(item.id)}
className="h-6 w-6 p-0 text-red-400 hover:text-red-300 hover:bg-red-500/20"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</div>
<div className="text-gray-300 text-xs mb-2 line-clamp-2">
{item.descricao}
</div>
<div className="grid grid-cols-3 gap-2 text-xs">
<div>
<div className="text-gray-400">Qtd:</div>
<div className="font-bold text-blue-400">{item.quantidade_expedida}</div>
</div>
<div>
<div className="text-gray-400">Peso Unit:</div>
<div className="text-gray-300">{item.peso_unitario.toFixed(2)} kg</div>
</div>
<div>
<div className="text-gray-400">Peso Total:</div>
<div className="font-medium text-white">{item.peso_total.toFixed(2)} kg</div>
</div>
</div>
</div>
) : (
// Layout desktop/tablet - table style
<div className={`grid gap-3 text-sm p-3 border-b border-slate-700 hover:bg-slate-800/50 transition-colors ${
index % 2 === 0 ? 'bg-slate-800/20' : ''
} ${isTablet ? 'grid-cols-8' : 'grid-cols-10'}`}>
<div className="font-medium text-white truncate">{item.marca}</div>
<div className="text-gray-300 truncate">{item.fase}</div>
<div className={`text-gray-300 truncate ${isTablet ? 'col-span-1' : 'col-span-2'}`}>
{item.descricao}
</div>
<div className="text-center font-bold text-blue-400">
{item.quantidade_expedida}
</div>
<div className="text-right text-gray-300">
{item.peso_unitario.toFixed(2)} kg
</div>
<div className="text-right font-medium text-white">
{item.peso_total.toFixed(2)} kg
</div>
{!isTablet && (
<div className="flex justify-center">
<Badge className={`text-xs ${getPrioridadeColor(item.prioridade)}`}>
{item.prioridade}
</Badge>
</div>
)}
<div className={`flex items-center justify-center gap-2 ${isTablet ? 'col-span-2' : 'col-span-2'}`}>
<Button
size="sm"
variant="ghost"
onClick={() => handleEditarItem(item)}
className="h-7 w-7 p-0 text-blue-400 hover:text-blue-300 hover:bg-blue-500/20"
>
<Edit2 className="h-3 w-3" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => handleRemoverItem(item.id)}
className="h-7 w-7 p-0 text-red-400 hover:text-red-300 hover:bg-red-500/20"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</div>
)}
</div>
))}
</div>
</ScrollArea>
)}
</div>
<div className={`flex justify-end pt-3 border-t border-slate-700 shrink-0 ${isMobile ? 'px-1' : ''}`}>
<Button
variant="outline"
onClick={onClose}
className={`border-slate-600 text-gray-300 hover:bg-slate-800 ${isMobile ? 'h-10 px-4 text-sm' : ''}`}
>
Fechar
</Button>
</div>
</DialogContent>
</Dialog>
);
};
@@ -0,0 +1,123 @@
import React from 'react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { AlertTriangle, Calendar } from 'lucide-react';
interface ProcessoPulado {
nome: string;
ordem: number;
dataRetroativa: string;
}
interface PecaComProcessosPulados {
peca_id: string;
marca: string;
processos_pulados: ProcessoPulado[];
processo_atual: string;
}
interface ProcessosPuladosModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
onCancel: () => void;
pecasComProcessosPulados: PecaComProcessosPulados[];
loading?: boolean;
}
export const ProcessosPuladosModal: React.FC<ProcessosPuladosModalProps> = ({
isOpen,
onClose,
onConfirm,
onCancel,
pecasComProcessosPulados,
loading = false
}) => {
const totalPecas = pecasComProcessosPulados.length;
const totalProcessosPulados = pecasComProcessosPulados.reduce(
(sum, peca) => sum + peca.processos_pulados.length,
0
);
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<AlertTriangle className="w-5 h-5 text-amber-500" />
Processos Anteriores Não Executados
</DialogTitle>
<DialogDescription>
Foram encontradas {totalPecas} peça(s) que não passaram por todos os processos anteriores.
O sistema pode criar apontamentos retroativos automaticamente.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
<h4 className="font-medium text-amber-800 mb-2">
⚠️ Ação Automática Proposta:
</h4>
<p className="text-sm text-amber-700">
• Total de processos que serão preenchidos automaticamente: <strong>{totalProcessosPulados}</strong>
</p>
<p className="text-sm text-amber-700">
• Datas retroativas serão aplicadas (2 dias por processo anterior)
</p>
</div>
<div className="space-y-3">
{pecasComProcessosPulados.map((peca, index) => (
<div key={index} className="border rounded-lg p-3">
<div className="flex items-center gap-2 mb-2">
<Badge variant="outline" className="font-mono">
{peca.marca}
</Badge>
<span className="text-sm text-muted-foreground">
Processo atual: {peca.processo_atual}
</span>
</div>
<div className="space-y-2">
<p className="text-sm font-medium">Processos que serão preenchidos automaticamente:</p>
{peca.processos_pulados.map((processo, pIndex) => (
<div key={pIndex} className="flex items-center gap-2 text-sm bg-gray-50 rounded p-2">
<Calendar className="w-4 h-4 text-blue-500" />
<span className="font-medium">{processo.nome}</span>
<span className="text-muted-foreground">•</span>
<span className="text-muted-foreground">
Data: {new Date(processo.dataRetroativa).toLocaleDateString('pt-BR')}
</span>
<Badge variant="secondary" className="text-xs">
Ordem {processo.ordem}
</Badge>
</div>
))}
</div>
</div>
))}
</div>
</div>
<DialogFooter className="flex gap-2">
<Button
variant="outline"
onClick={onCancel}
disabled={loading}
>
Cancelar Apontamento
</Button>
<Button
onClick={onConfirm}
disabled={loading}
className="bg-amber-600 hover:bg-amber-700"
>
{loading ? 'Processando...' : 'Confirmar e Criar Apontamentos'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
@@ -0,0 +1,278 @@
import React from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { FileDown, FileSpreadsheet, Package } from 'lucide-react';
import { useRomaneios } from '@/hooks/useRomaneios';
import { generateProfessionalPDF } from '@/utils/pdfGenerator';
import { generateCSV, downloadCSV } from '@/utils/csvUtils';
import { toast } from 'sonner';
interface RelatorioGeralModalProps {
isOpen: boolean;
onClose: () => void;
ofNumber: string;
}
export const RelatorioGeralModal: React.FC<RelatorioGeralModalProps> = ({
isOpen,
onClose,
ofNumber
}) => {
const { data: romaneios } = useRomaneios();
const romaneiosDaOF = romaneios?.filter(r => r.of_number === ofNumber) || [];
const totalRomaneios = romaneiosDaOF.length;
const pesoTotalPecas = romaneiosDaOF.reduce((sum, r) => {
return sum + (r.itens_pecas?.reduce((s, item) => s + (item.peso_total || 0), 0) || 0);
}, 0);
const pesoTotalInsumos = romaneiosDaOF.reduce((sum, r) => {
return sum + (r.itens_insumos?.reduce((s, item) => s + (item.peso_total || 0), 0) || 0);
}, 0);
const pesoTotalGeral = pesoTotalPecas + pesoTotalInsumos;
// Consolidar peças por marca e fase
const pecasConsolidadas = romaneiosDaOF.reduce((acc, romaneio) => {
romaneio.itens_pecas?.forEach(item => {
const key = `${item.marca}-${item.fase || 'N/A'}`;
if (acc[key]) {
acc[key].quantidade += item.quantidade_expedida;
acc[key].peso_total += item.peso_total || 0;
} else {
acc[key] = {
marca: item.marca,
fase: item.fase || 'N/A',
quantidade: item.quantidade_expedida,
peso_total: item.peso_total || 0
};
}
});
return acc;
}, {} as Record<string, any>);
// Consolidar insumos por tipo
const insumosConsolidados = romaneiosDaOF.reduce((acc, romaneio) => {
romaneio.itens_insumos?.forEach(item => {
const key = item.tipo_insumo;
if (acc[key]) {
acc[key].quantidade_total += item.quantidade_expedida;
acc[key].itens.push(item);
} else {
acc[key] = {
tipo: item.tipo_insumo,
quantidade_total: item.quantidade_expedida,
unidade: item.unidade,
itens: [item]
};
}
});
return acc;
}, {} as Record<string, any>);
const handleExportPDF = async () => {
try {
const filename = `relatorio_geral_romaneios_${ofNumber}_${new Date().toISOString().split('T')[0]}.pdf`;
await generateProfessionalPDF('relatorio-geral-preview', filename);
toast.success('PDF gerado com sucesso!');
} catch (error) {
console.error('Erro ao gerar PDF:', error);
toast.error('Erro ao gerar PDF');
}
};
const handleExportCSV = () => {
try {
const headers = [
'Número Romaneio',
'OF',
'Data Criação',
'Status',
'Peso Total (kg)',
'Tipo Transporte',
'Observações'
];
const csvData = romaneiosDaOF.map(r => ({
'Número Romaneio': r.numero_romaneio,
'OF': r.of_number,
'Data Criação': new Date(r.data_criacao).toLocaleDateString('pt-BR'),
'Status': r.status,
'Peso Total (kg)': r.peso_total_romaneio || 0,
'Tipo Transporte': r.tipo_transporte || 'N/A',
'Observações': r.observacoes || 'N/A'
}));
const csvContent = generateCSV(csvData, headers);
const filename = `relatorio_geral_romaneios_${ofNumber}_${new Date().toISOString().split('T')[0]}.csv`;
downloadCSV(csvContent, filename);
toast.success('Planilha exportada com sucesso!');
} catch (error) {
console.error('Erro ao exportar CSV:', error);
toast.error('Erro ao exportar planilha');
}
};
return (
<>
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Package className="w-5 h-5" />
Relatório Geral de Romaneios - OF {ofNumber}
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
{/* Preview do Relatório */}
<div id="relatorio-geral-preview" className="bg-white p-8 rounded-lg border" style={{ fontFamily: 'Inter, sans-serif' }}>
{/* Cabeçalho do Relatório */}
<div className="flex justify-between items-start border-b-2 border-gray-300 pb-4 mb-6">
<div>
<h1 className="text-2xl font-semibold text-gray-800">Relatório Geral de Romaneios por OF</h1>
<p className="text-lg text-gray-700">Contrato: <span className="font-bold">{ofNumber}</span></p>
</div>
<div className="text-right">
<p className="text-sm text-gray-500">Data de Emissão: <span>{new Date().toLocaleDateString('pt-BR')}</span></p>
</div>
</div>
{/* Resumo Quantitativo */}
<h2 className="text-xl font-semibold mb-3">Resumo Quantitativo</h2>
<div className="grid grid-cols-4 gap-4 mb-6">
<div className="bg-gray-50 border border-gray-200 rounded-lg p-3 text-center">
<div className="text-xl font-bold text-gray-800">{totalRomaneios}</div>
<div className="text-sm text-gray-600">Total de Romaneios</div>
</div>
<div className="bg-gray-50 border border-gray-200 rounded-lg p-3 text-center">
<div className="text-xl font-bold text-gray-800">{pesoTotalPecas.toFixed(0)} kg</div>
<div className="text-sm text-gray-600">Peso Total (Peças)</div>
</div>
<div className="bg-gray-50 border border-gray-200 rounded-lg p-3 text-center">
<div className="text-xl font-bold text-gray-800">{pesoTotalInsumos.toFixed(0)} kg</div>
<div className="text-sm text-gray-600">Peso Total (Insumos)</div>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3 text-center">
<div className="text-xl font-bold text-gray-800">{pesoTotalGeral.toFixed(0)} kg</div>
<div className="text-sm text-gray-600">Peso Total Geral</div>
</div>
</div>
{/* Lista de Romaneios Emitidos */}
<h2 className="text-xl font-semibold mb-3">Romaneios Emitidos</h2>
<div className="mb-6">
<table className="w-full border-collapse border border-gray-300">
<thead>
<tr className="bg-gray-50">
<th className="border border-gray-300 px-2 py-1.5 text-left text-sm font-medium">Número</th>
<th className="border border-gray-300 px-2 py-1.5 text-left text-sm font-medium">Data</th>
<th className="border border-gray-300 px-2 py-1.5 text-left text-sm font-medium">Status</th>
<th className="border border-gray-300 px-2 py-1.5 text-left text-sm font-medium">Peso Peças</th>
<th className="border border-gray-300 px-2 py-1.5 text-left text-sm font-medium">Transporte</th>
</tr>
</thead>
<tbody>
{romaneiosDaOF.map((romaneio) => {
const pesoPecas = romaneio.itens_pecas?.reduce((sum, item) => sum + (item.peso_total || 0), 0) || 0;
return (
<tr key={romaneio.id}>
<td className="border border-gray-300 px-2 py-1.5 text-xs">{romaneio.numero_romaneio}</td>
<td className="border border-gray-300 px-2 py-1.5 text-xs">
{new Date(romaneio.data_criacao).toLocaleDateString('pt-BR')}
</td>
<td className="border border-gray-300 px-2 py-1.5 text-xs">{romaneio.status}</td>
<td className="border border-gray-300 px-2 py-1.5 text-xs">{pesoPecas.toFixed(0)} kg</td>
<td className="border border-gray-300 px-2 py-1.5 text-xs">{romaneio.tipo_transporte || 'N/A'}</td>
</tr>
);
})}
</tbody>
</table>
</div>
{/* Seção de Peças e Insumos em Colunas */}
<div className="grid grid-cols-2 gap-6 mt-10">
{/* Coluna de Peças */}
<div>
<h2 className="text-xl font-semibold mb-3">Resumo de Peças Expedidas</h2>
<h3 className="text-lg font-medium mb-2">Total por Marca e Fase</h3>
<table className="w-full border-collapse border border-gray-300">
<thead>
<tr className="bg-gray-50">
<th className="border border-gray-300 px-2 py-1.5 text-left text-sm font-medium">Marca</th>
<th className="border border-gray-300 px-2 py-1.5 text-left text-sm font-medium">Fase</th>
<th className="border border-gray-300 px-2 py-1.5 text-left text-sm font-medium">Qtd.</th>
<th className="border border-gray-300 px-2 py-1.5 text-left text-sm font-medium">Peso Total</th>
</tr>
</thead>
<tbody>
{Object.values(pecasConsolidadas).map((peca: any, index) => (
<tr key={index}>
<td className="border border-gray-300 px-2 py-1.5 text-xs">{peca.marca}</td>
<td className="border border-gray-300 px-2 py-1.5 text-xs">{peca.fase}</td>
<td className="border border-gray-300 px-2 py-1.5 text-xs">{peca.quantidade}</td>
<td className="border border-gray-300 px-2 py-1.5 text-xs">{peca.peso_total.toFixed(0)} kg</td>
</tr>
))}
<tr className="bg-gray-50">
<td className="border border-gray-300 px-2 py-1.5 text-xs font-bold">TOTAL</td>
<td className="border border-gray-300 px-2 py-1.5 text-xs"></td>
<td className="border border-gray-300 px-2 py-1.5 text-xs font-bold">
{Object.values(pecasConsolidadas).reduce((sum: number, peca: any) => sum + peca.quantidade, 0)}
</td>
<td className="border border-gray-300 px-2 py-1.5 text-xs font-bold">
{Object.values(pecasConsolidadas).reduce((sum: number, peca: any) => sum + peca.peso_total, 0).toFixed(0)} kg
</td>
</tr>
</tbody>
</table>
</div>
{/* Coluna de Insumos */}
<div>
<h2 className="text-xl font-semibold mb-3">Resumo de Insumos Expedidos</h2>
<h3 className="text-lg font-medium mb-2">Total por Tipo</h3>
<table className="w-full border-collapse border border-gray-300">
<thead>
<tr className="bg-gray-50">
<th className="border border-gray-300 px-2 py-1.5 text-left text-sm font-medium">Tipo</th>
<th className="border border-gray-300 px-2 py-1.5 text-left text-sm font-medium">Descrição</th>
<th className="border border-gray-300 px-2 py-1.5 text-left text-sm font-medium">Qtd. Total</th>
<th className="border border-gray-300 px-2 py-1.5 text-left text-sm font-medium">Unidade</th>
</tr>
</thead>
<tbody>
{Object.values(insumosConsolidados).map((insumo: any, index) => (
<tr key={index}>
<td className="border border-gray-300 px-2 py-1.5 text-xs">{insumo.tipo}</td>
<td className="border border-gray-300 px-2 py-1.5 text-xs">
{insumo.itens.map((item: any) => item.descricao).join(', ')}
</td>
<td className="border border-gray-300 px-2 py-1.5 text-xs">{insumo.quantidade_total}</td>
<td className="border border-gray-300 px-2 py-1.5 text-xs">{insumo.unidade}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
{/* Botões de Exportação */}
<div className="flex gap-4 justify-center">
<Button onClick={handleExportPDF} className="flex items-center gap-2">
<FileDown className="w-4 h-4" />
Exportar PDF
</Button>
<Button onClick={handleExportCSV} variant="outline" className="flex items-center gap-2">
<FileSpreadsheet className="w-4 h-4" />
Exportar Planilha
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</>
);
};
@@ -0,0 +1,259 @@
import React from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { FileDown, FileSpreadsheet, Package2 } from 'lucide-react';
import { useApontamentosExpedicao } from '@/hooks/useApontamentosExpedicao';
import { generateProfessionalPDF } from '@/utils/pdfGenerator';
import { generateCSV, downloadCSV } from '@/utils/csvUtils';
import { toast } from 'sonner';
interface RelatorioPecasModalProps {
isOpen: boolean;
onClose: () => void;
ofNumber: string;
}
export const RelatorioPecasModal: React.FC<RelatorioPecasModalProps> = ({
isOpen,
onClose,
ofNumber
}) => {
const { apontamentosExpedicao, pecasCompletas, loading } = useApontamentosExpedicao(ofNumber);
// Combinar dados das peças cadastradas com as expedidas
const relatorioPecas = pecasCompletas.map(peca => {
// Buscar apontamento correspondente pela combinação OF + Fase + Marca
const apontamentoExpedicao = apontamentosExpedicao.find(apt =>
apt.of_number === peca.of_number &&
apt.etapa_fase === (peca.etapa_fase || '') &&
apt.marca === peca.marca
);
const quantidadeExpedida = apontamentoExpedicao?.quantidade_expedida || 0;
const pesoTotalExpedido = apontamentoExpedicao?.peso_total_expedido || 0;
const quantidadePendente = peca.quantidade_total - quantidadeExpedida;
const pesoTotalPendente = quantidadePendente * peca.peso_unitario;
return {
of_number: peca.of_number,
etapa_fase: peca.etapa_fase || '',
marca: peca.marca,
descricao: peca.descricao || 'N/A',
quantidade_total: peca.quantidade_total,
quantidade_expedida: quantidadeExpedida,
peso_total_expedido: pesoTotalExpedido,
quantidade_pendente: quantidadePendente,
peso_total_pendente: pesoTotalPendente,
status: quantidadeExpedida === 0 ? 'Pendente' :
quantidadeExpedida >= peca.quantidade_total ? 'Concluído' : 'Parcial'
};
});
// Calcular totais
const totalPecasExpedidas = relatorioPecas.reduce((sum, peca) => sum + peca.quantidade_expedida, 0);
const pesoTotalExpedido = relatorioPecas.reduce((sum, peca) => sum + peca.peso_total_expedido, 0);
const totalPecasPendentes = relatorioPecas.reduce((sum, peca) => sum + peca.quantidade_pendente, 0);
const pesoTotalPendente = relatorioPecas.reduce((sum, peca) => sum + peca.peso_total_pendente, 0);
const handleExportPDF = async () => {
try {
const element = document.getElementById('relatorio-pecas-expedicao-preview');
if (!element) {
toast.error('Erro: Elemento do relatório não encontrado');
return;
}
// Aguardar um momento para garantir que o elemento esteja renderizado
await new Promise(resolve => setTimeout(resolve, 100));
const filename = `relatorio_pecas_expedicao_${ofNumber}_${new Date().toISOString().split('T')[0]}.pdf`;
// Usar a função de geração de PDF profissional
await generateProfessionalPDF('relatorio-pecas-expedicao-preview', filename);
toast.success('PDF baixado com sucesso!');
} catch (error) {
console.error('Erro ao gerar PDF:', error);
toast.error('Erro ao gerar PDF. Tente novamente.');
}
};
const handleExportCSV = () => {
try {
const headers = [
'OF',
'Fase',
'Marca',
'Descrição',
'Qtd. Total',
'Qtd. Expedida',
'Peso Total Expedido (kg)',
'Qtd. Pendente',
'Peso Total Pendente (kg)',
'Status'
];
const csvData = relatorioPecas.map(peca => ({
'OF': peca.of_number,
'Fase': peca.etapa_fase,
'Marca': peca.marca,
'Descrição': peca.descricao,
'Qtd. Total': peca.quantidade_total,
'Qtd. Expedida': peca.quantidade_expedida,
'Peso Total Expedido (kg)': peca.peso_total_expedido.toFixed(2),
'Qtd. Pendente': peca.quantidade_pendente,
'Peso Total Pendente (kg)': peca.peso_total_pendente.toFixed(2),
'Status': peca.status
}));
const csvContent = generateCSV(csvData, headers);
const filename = `relatorio_pecas_expedicao_${ofNumber}_${new Date().toISOString().split('T')[0]}.csv`;
downloadCSV(csvContent, filename);
toast.success('Planilha exportada com sucesso!');
} catch (error) {
console.error('Erro ao exportar CSV:', error);
toast.error('Erro ao exportar planilha');
}
};
if (loading) {
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Package2 className="w-5 h-5" />
Relatório de Expedição de Peças - OF {ofNumber}
</DialogTitle>
</DialogHeader>
<div className="flex items-center justify-center p-8">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
<p className="text-muted-foreground">Carregando dados...</p>
</div>
</div>
</DialogContent>
</Dialog>
);
}
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Package2 className="w-5 h-5" />
Relatório de Expedição de Peças - OF {ofNumber}
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
{/* Preview do Relatório */}
<div id="relatorio-pecas-expedicao-preview" className="bg-white p-6 rounded-lg border" style={{ fontFamily: 'Inter, sans-serif' }}>
{/* Cabeçalho do Relatório */}
<div className="flex justify-between items-start border-b-2 border-gray-300 pb-4 mb-4">
<div>
<h1 className="text-2xl font-semibold text-gray-800">Relatório de Expedição de Peças</h1>
<p className="text-lg text-gray-700">Contrato/OF: <span className="font-bold">{ofNumber}</span></p>
</div>
<div className="text-right">
<p className="text-sm text-gray-500">Data de Emissão: <span className="font-medium">{new Date().toLocaleDateString('pt-BR')}</span></p>
</div>
</div>
{/* Resumo Geral de Peças */}
<h2 className="text-xl font-semibold mb-3 text-gray-800">Resumo Geral de Peças</h2>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div className="bg-gray-50 border border-gray-200 rounded-lg p-3 text-center">
<div className="text-2xl font-bold text-gray-900">{totalPecasExpedidas}</div>
<div className="text-sm text-gray-600">Total de Peças Expedidas</div>
</div>
<div className="bg-gray-50 border border-gray-200 rounded-lg p-3 text-center">
<div className="text-2xl font-bold text-gray-900">{pesoTotalExpedido.toFixed(0)} kg</div>
<div className="text-sm text-gray-600">Peso Total Expedido</div>
</div>
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3 text-center">
<div className="text-2xl font-bold text-gray-900">{totalPecasPendentes}</div>
<div className="text-sm text-gray-600">Total de Peças Pendentes</div>
</div>
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3 text-center">
<div className="text-2xl font-bold text-gray-900">{pesoTotalPendente.toFixed(0)} kg</div>
<div className="text-sm text-gray-600">Peso Total Pendente</div>
</div>
</div>
{/* Tabela de Peças */}
<div className="overflow-x-auto mt-6">
<table className="w-full border-collapse">
<thead>
<tr className="bg-gray-50">
<th className="border border-gray-300 p-2 text-left text-sm font-medium text-gray-700">OF</th>
<th className="border border-gray-300 p-2 text-left text-sm font-medium text-gray-700">Fase</th>
<th className="border border-gray-300 p-2 text-left text-sm font-medium text-gray-700">Marca</th>
<th className="border border-gray-300 p-2 text-left text-sm font-medium text-gray-700">Descrição</th>
<th className="border border-gray-300 p-2 text-left text-sm font-medium text-gray-700">Qtd. Total</th>
<th className="border border-gray-300 p-2 text-left text-sm font-medium text-gray-700">Qtd. Expedida</th>
<th className="border border-gray-300 p-2 text-left text-sm font-medium text-gray-700">Peso Total Exp.</th>
<th className="border border-gray-300 p-2 text-left text-sm font-medium text-gray-700">Qtd. Pendente</th>
<th className="border border-gray-300 p-2 text-left text-sm font-medium text-gray-700">Peso Total Pend.</th>
<th className="border border-gray-300 p-2 text-left text-sm font-medium text-gray-700">Status</th>
</tr>
</thead>
<tbody>
{relatorioPecas.map((peca, index) => (
<tr key={`${peca.of_number}-${peca.etapa_fase}-${peca.marca}-${index}`} className="hover:bg-gray-50">
<td className="border border-gray-300 p-2 text-sm">{peca.of_number}</td>
<td className="border border-gray-300 p-2 text-sm">{peca.etapa_fase}</td>
<td className="border border-gray-300 p-2 text-sm">{peca.marca}</td>
<td className="border border-gray-300 p-2 text-sm">{peca.descricao}</td>
<td className="border border-gray-300 p-2 text-sm">{peca.quantidade_total}</td>
<td className="border border-gray-300 p-2 text-sm">{peca.quantidade_expedida}</td>
<td className="border border-gray-300 p-2 text-sm">{peca.peso_total_expedido.toFixed(0)} kg</td>
<td className="border border-gray-300 p-2 text-sm">{peca.quantidade_pendente}</td>
<td className="border border-gray-300 p-2 text-sm">{peca.peso_total_pendente.toFixed(0)} kg</td>
<td className="border border-gray-300 p-2 text-sm">
<span className={`inline-block px-2 py-1 rounded-full text-xs font-medium ${
peca.status === 'Concluído'
? 'bg-green-100 text-green-800'
: peca.status === 'Parcial'
? 'bg-yellow-100 text-yellow-800'
: 'bg-gray-100 text-gray-800'
}`}>
{peca.status}
</span>
</td>
</tr>
))}
{/* Linha de Total */}
<tr className="bg-gray-50 font-bold">
<td className="border border-gray-300 p-2 text-sm" colSpan={5}>TOTAIS</td>
<td className="border border-gray-300 p-2 text-sm">{totalPecasExpedidas}</td>
<td className="border border-gray-300 p-2 text-sm">{pesoTotalExpedido.toFixed(0)} kg</td>
<td className="border border-gray-300 p-2 text-sm">{totalPecasPendentes}</td>
<td className="border border-gray-300 p-2 text-sm">{pesoTotalPendente.toFixed(0)} kg</td>
<td className="border border-gray-300 p-2 text-sm"></td>
</tr>
</tbody>
</table>
</div>
</div>
{/* Botões de Exportação */}
<div className="flex gap-4 justify-center">
<Button
onClick={handleExportPDF}
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white"
>
<FileDown className="w-4 h-4" />
Exportar PDF
</Button>
<Button onClick={handleExportCSV} variant="outline" className="flex items-center gap-2">
<FileSpreadsheet className="w-4 h-4" />
Exportar Planilha
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
};
@@ -0,0 +1,96 @@
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { FileText, BarChart3 } from 'lucide-react';
import { useOFsAtivas } from '@/hooks/useOFsAtivas';
import { RelatorioGeralModal } from './RelatorioGeralModal';
import { RelatorioPecasModal } from './RelatorioPecasModal';
export const RelatoriosExpedicao = () => {
const { data: ofsAtivas } = useOFsAtivas();
const [selectedOF, setSelectedOF] = useState<string>('');
const [showRelatorioGeral, setShowRelatorioGeral] = useState(false);
const [showRelatorioPecas, setShowRelatorioPecas] = useState(false);
const handleGerarRelatorioGeral = () => {
if (!selectedOF) {
alert('Por favor, selecione uma OF para gerar o relatório');
return;
}
setShowRelatorioGeral(true);
};
const handleGerarRelatorioPecas = () => {
if (!selectedOF) {
alert('Por favor, selecione uma OF para gerar o relatório');
return;
}
setShowRelatorioPecas(true);
};
return (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="text-lg font-semibold">Relatórios de Expedição</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium">Selecione a OF:</label>
<Select value={selectedOF} onValueChange={setSelectedOF}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Selecione uma OF..." />
</SelectTrigger>
<SelectContent>
{ofsAtivas?.map((of) => (
<SelectItem key={of.of_number} value={of.of_number}>
{of.of_number} - {of.descricao_resumida || of.cliente}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex gap-4 flex-wrap">
<Button
onClick={handleGerarRelatorioGeral}
disabled={!selectedOF}
className="flex items-center gap-2"
>
<FileText className="w-4 h-4" />
Relatório Geral de Romaneios da OF
</Button>
<Button
onClick={handleGerarRelatorioPecas}
disabled={!selectedOF}
variant="outline"
className="flex items-center gap-2"
>
<BarChart3 className="w-4 h-4" />
Relatório de Peças por OF
</Button>
</div>
</CardContent>
</Card>
{showRelatorioGeral && (
<RelatorioGeralModal
isOpen={showRelatorioGeral}
onClose={() => setShowRelatorioGeral(false)}
ofNumber={selectedOF}
/>
)}
{showRelatorioPecas && (
<RelatorioPecasModal
isOpen={showRelatorioPecas}
onClose={() => setShowRelatorioPecas(false)}
ofNumber={selectedOF}
/>
)}
</div>
);
};
+349
View File
@@ -0,0 +1,349 @@
import React, { useState } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Save, X, AlertTriangle } from 'lucide-react';
import { RomaneioExpedicao } from '@/hooks/useRomaneios';
interface RomaneioFormProps {
romaneio?: RomaneioExpedicao;
ofNumbers: string[];
selectedOF?: string;
onSave: (data: any) => void;
onCancel: () => void;
loading: boolean;
isOpen: boolean;
onClose: () => void;
}
export function RomaneioForm({ romaneio, ofNumbers, selectedOF, onSave, onCancel, loading, isOpen, onClose }: RomaneioFormProps) {
console.log('📋 RomaneioForm renderizado com OFs:', ofNumbers);
// Helper function to format date for input (YYYY-MM-DD)
const formatDateForInput = (date: string | Date) => {
if (!date) return '';
const d = new Date(date);
return d.toISOString().split('T')[0];
};
// Helper function to get current date in local timezone
const getCurrentLocalDate = () => {
const now = new Date();
return now.toISOString().split('T')[0];
};
const [formData, setFormData] = useState({
of_number: romaneio?.of_number || selectedOF || '',
data_romaneio: romaneio?.data_romaneio ? formatDateForInput(romaneio.data_romaneio) : getCurrentLocalDate(),
data_criacao: romaneio?.data_criacao ? formatDateForInput(romaneio.data_criacao) : getCurrentLocalDate(),
revisao: romaneio?.revisao || 0,
motivo_revisao: romaneio?.motivo_revisao || '',
data_prevista_entrega: romaneio?.data_prevista_entrega ? formatDateForInput(romaneio.data_prevista_entrega) : '',
prioridade: romaneio?.prioridade || 'Normal' as 'Normal' | 'Urgente',
status: romaneio?.status || 'Em planejamento' as 'Em planejamento' | 'Entregue' | 'Conferido em Obra',
observacoes: romaneio?.observacoes || '',
peso_total_romaneio: romaneio?.peso_total_romaneio || 0,
previsao_kg: romaneio?.previsao_kg || 0,
maior_dimensao: romaneio?.maior_dimensao || '',
tipo_transporte: romaneio?.tipo_transporte || 'carro' as 'carro' | 'utilitario' | 'caminho_pequeno' | 'caminhao_trucado' | 'caminhao_munck' | 'carreta_12m' | 'carreta_15m' | 'especial',
frete_tipo: romaneio?.frete_tipo || 'terceiros' as 'terceiros' | 'proprio',
nome_motorista: romaneio?.nome_motorista || ''
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log('💾 Salvando romaneio com dados:', formData);
// Preparar dados para envio
const dataToSave = {
...formData,
// Garantir que nome_motorista não fique vazio
nome_motorista: formData.nome_motorista.trim() || 'sem_definição',
// Garantir que a revisão seja um número
revisao: parseInt(formData.revisao.toString()) || 0
};
onSave(dataToSave);
};
const handleCancel = () => {
onCancel();
onClose();
};
const statusOptions = [
'Em planejamento',
'Entregue',
'Conferido em Obra'
];
const transporteOptions = [
{ value: 'carro', label: 'Carro' },
{ value: 'utilitario', label: 'Utilitário' },
{ value: 'caminho_pequeno', label: 'Caminhão Pequeno' },
{ value: 'caminhao_trucado', label: 'Caminhão Trucado' },
{ value: 'caminhao_munck', label: 'Caminhão Munck' },
{ value: 'carreta_12m', label: 'Carreta 12m' },
{ value: 'carreta_15m', label: 'Carreta 15m' },
{ value: 'especial', label: 'Especial' }
];
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-hidden">
<DialogHeader>
<div className="flex items-center justify-between">
<DialogTitle>
{romaneio ? 'Editar Romaneio' : 'Novo Romaneio'}
</DialogTitle>
<Button variant="outline" onClick={handleCancel} size="sm">
<X className="h-4 w-4 mr-2" />
Fechar
</Button>
</div>
</DialogHeader>
<ScrollArea className="max-h-[calc(90vh-120px)] pr-4">
<form onSubmit={handleSubmit} className="space-y-6">
{/* Seção: Dados Básicos */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-foreground border-b pb-2">Dados Básicos</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="of_number">Ordem de Fabricação *</Label>
<Select
value={formData.of_number}
onValueChange={(value) => setFormData(prev => ({ ...prev, of_number: value }))}
disabled={!!romaneio || !!selectedOF}
>
<SelectTrigger>
<SelectValue placeholder="Selecionar OF" />
</SelectTrigger>
<SelectContent>
{ofNumbers.length === 0 ? (
<SelectItem value="" disabled>
Nenhuma OF disponível
</SelectItem>
) : (
ofNumbers.map((ofNumber) => (
<SelectItem key={ofNumber} value={ofNumber}>
{ofNumber}
</SelectItem>
))
)}
</SelectContent>
</Select>
{ofNumbers.length === 0 && (
<p className="text-xs text-muted-foreground">
Nenhuma OF encontrada. Verifique suas permissões ou contate o administrador.
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="data_criacao">Data de Criação *</Label>
<Input
id="data_criacao"
type="date"
value={formData.data_criacao}
onChange={(e) => setFormData(prev => ({ ...prev, data_criacao: e.target.value }))}
/>
</div>
<div className="space-y-2">
<Label htmlFor="data_romaneio">Data do Romaneio</Label>
<Input
id="data_romaneio"
type="date"
value={formData.data_romaneio}
onChange={(e) => setFormData(prev => ({ ...prev, data_romaneio: e.target.value }))}
/>
</div>
<div className="space-y-2">
<Label htmlFor="revisao">Revisão</Label>
<Input
id="revisao"
type="number"
min="0"
value={formData.revisao}
onChange={(e) => setFormData(prev => ({ ...prev, revisao: parseInt(e.target.value) || 0 }))}
/>
</div>
<div className="space-y-2">
<Label htmlFor="data_prevista_entrega">Data Prevista de Entrega</Label>
<Input
id="data_prevista_entrega"
type="date"
value={formData.data_prevista_entrega}
onChange={(e) => setFormData(prev => ({ ...prev, data_prevista_entrega: e.target.value }))}
/>
</div>
<div className="space-y-2">
<Label htmlFor="prioridade">Prioridade</Label>
<Select
value={formData.prioridade}
onValueChange={(value: 'Normal' | 'Urgente') => setFormData(prev => ({ ...prev, prioridade: value }))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="Normal">Normal</SelectItem>
<SelectItem value="Urgente">Urgente</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="status">Status</Label>
<Select
value={formData.status}
onValueChange={(value: any) => setFormData(prev => ({ ...prev, status: value }))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{statusOptions.map((status) => (
<SelectItem key={status} value={status}>
{status}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Badge de Alerta */}
<Alert className="border-orange-500 bg-orange-50 dark:bg-orange-950/20 dark:border-orange-400 mt-4">
<AlertTriangle className="h-4 w-4 text-orange-600 dark:text-orange-400" />
<AlertDescription className="text-orange-800 dark:text-orange-200 font-medium">
QUANDO O STATUS PASSAR PARA "ENTREGUE" AS PEÇAS SERÃO APONTADAS AUTOMATICAMENTE NO PROCESSO DE EXPEDIÇÃO
</AlertDescription>
</Alert>
</div>
{/* Seção: Dados de Transporte */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-foreground border-b pb-2">Dados de Transporte</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="previsao_kg">Previsão de Kg</Label>
<Input
id="previsao_kg"
type="number"
min="0"
step="0.01"
value={formData.previsao_kg}
onChange={(e) => setFormData(prev => ({ ...prev, previsao_kg: parseFloat(e.target.value) || 0 }))}
/>
</div>
<div className="space-y-2">
<Label htmlFor="maior_dimensao">Maior Dimensão</Label>
<Input
id="maior_dimensao"
value={formData.maior_dimensao}
onChange={(e) => setFormData(prev => ({ ...prev, maior_dimensao: e.target.value }))}
placeholder="Ex: 12m x 2m x 3m"
/>
</div>
<div className="space-y-2">
<Label htmlFor="tipo_transporte">Tipo de Transporte</Label>
<Select
value={formData.tipo_transporte}
onValueChange={(value: any) => setFormData(prev => ({ ...prev, tipo_transporte: value }))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{transporteOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="frete_tipo">Tipo de Frete</Label>
<Select
value={formData.frete_tipo}
onValueChange={(value: 'terceiros' | 'proprio') => setFormData(prev => ({ ...prev, frete_tipo: value }))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="terceiros">Terceiros</SelectItem>
<SelectItem value="proprio">Próprio</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="nome_motorista">Nome do Motorista</Label>
<Input
id="nome_motorista"
value={formData.nome_motorista}
onChange={(e) => setFormData(prev => ({ ...prev, nome_motorista: e.target.value }))}
placeholder="Nome completo do motorista (opcional)"
/>
<p className="text-xs text-muted-foreground">
Se não preenchido, será definido automaticamente como "sem_definição"
</p>
</div>
</div>
</div>
{/* Seção: Observações */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-foreground border-b pb-2">Observações</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="motivo_revisao">Motivo da Revisão</Label>
<Textarea
id="motivo_revisao"
value={formData.motivo_revisao}
onChange={(e) => setFormData(prev => ({ ...prev, motivo_revisao: e.target.value }))}
placeholder="Descreva o motivo da revisão..."
rows={3}
/>
</div>
<div className="space-y-2">
<Label htmlFor="observacoes">Observações Gerais</Label>
<Textarea
id="observacoes"
value={formData.observacoes}
onChange={(e) => setFormData(prev => ({ ...prev, observacoes: e.target.value }))}
placeholder="Observações do romaneio..."
rows={3}
/>
</div>
</div>
</div>
<div className="flex gap-2 pt-4">
<Button type="submit" disabled={loading || !formData.of_number}>
<Save className="h-4 w-4 mr-2" />
{loading ? 'Salvando...' : 'Salvar'}
</Button>
</div>
</form>
</ScrollArea>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,271 @@
import React from 'react';
import { RomaneioExpedicao } from '@/hooks/useRomaneios';
interface RomaneioPDFTemplateProps {
romaneio: RomaneioExpedicao;
}
export const RomaneioPDFTemplate: React.FC<RomaneioPDFTemplateProps> = ({ romaneio }) => {
const totalPecas = romaneio.itens_pecas?.reduce((sum, item) => sum + item.quantidade_expedida, 0) || 0;
const pesoTotalPecas = romaneio.itens_pecas?.reduce((sum, item) => sum + item.peso_total, 0) || 0;
const pesoTotalInsumos = romaneio.itens_insumos?.reduce((sum, item) => sum + (item.peso_total || 0), 0) || 0;
const pesoTotalGeral = pesoTotalPecas + pesoTotalInsumos;
const totalItens = totalPecas + (romaneio.itens_insumos?.reduce((sum, item) => sum + item.quantidade_expedida, 0) || 0);
const previsaoKg = romaneio.previsao_kg || 0;
const discrepancia = pesoTotalGeral - previsaoKg;
const formatDate = (dateStr: string) => {
if (!dateStr) return 'N/A';
return new Date(dateStr).toLocaleDateString('pt-BR');
};
const formatTransporte = (tipo: string) => {
const tipos: Record<string, string> = {
'carro': 'Carro',
'utilitario': 'Utilitário',
'caminho_pequeno': 'Caminhão Pequeno',
'caminhao_trucado': 'Caminhão Trucado',
'caminhao_munck': 'Caminhão Munck',
'carreta_12m': 'Carreta 12m',
'carreta_15m': 'Carreta 15m',
'especial': 'Especial'
};
return tipos[tipo] || tipo;
};
const formatFrete = (tipo: string) => {
return tipo === 'proprio' ? 'Próprio' : 'Terceiros';
};
return (
<div id="pdf-template" className="bg-slate-50 text-slate-800 font-sans">
<div className="container mx-auto p-3 md:p-6 max-w-full">
<header className="mb-6">
<h1 className="text-4xl font-bold text-slate-900">
Romaneio de Expedição: {romaneio.numero_romaneio}
</h1>
</header>
<div className="flex flex-col md:flex-row gap-4">
{/* Coluna Esquerda: KPIs Compactos - Largura reduzida e altura diminuída */}
<aside className="w-full md:w-32 flex-shrink-0">
<div className="space-y-1.5">
<div className="bg-white p-1.5 rounded-lg shadow-sm flex items-center">
<div className="bg-sky-100 text-sky-600 rounded-lg p-1 mr-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/>
</svg>
</div>
<div>
<p className="text-xs text-slate-500">Qtd. Total</p>
<p className="text-sm font-bold">{totalItens}</p>
</div>
</div>
<div className="bg-white p-1.5 rounded-lg shadow-sm flex items-center">
<div className="bg-sky-100 text-sky-600 rounded-lg p-1 mr-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l-3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3"/>
</svg>
</div>
<div>
<p className="text-xs text-slate-500">Peso Total</p>
<p className="text-sm font-bold">{pesoTotalGeral.toFixed(2)} Kg</p>
</div>
</div>
<div className="bg-white p-1.5 rounded-lg shadow-sm flex items-center">
<div className="bg-sky-100 text-sky-600 rounded-lg p-1 mr-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div>
<p className="text-xs text-slate-500">Status</p>
<p className="text-sm font-bold">{romaneio.status}</p>
</div>
</div>
</div>
</aside>
{/* Conteúdo Principal - Expandido para ocupar melhor a largura */}
<main className="flex-grow space-y-6 -ml-6">
{/* Seção de Detalhes e Observações - Expandida para ocupar mais largura */}
<section className="bg-white p-6 rounded-lg shadow-sm">
<div className="columns-1 md:columns-2 lg:columns-3 gap-x-8 text-base">
<div className="mb-6 break-inside-avoid-column">
<h3 className="font-semibold text-slate-800 mb-3 border-b pb-2 text-lg">Detalhes da Expedição</h3>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-base">Data Criação:</span>
<span className="font-medium text-slate-600 text-base">{formatDate(romaneio.data_criacao)}</span>
</div>
<div className="flex justify-between">
<span className="text-base">Data Romaneio:</span>
<span className="font-medium text-slate-600 text-base">{formatDate(romaneio.data_romaneio)}</span>
</div>
<div className="flex justify-between">
<span className="text-base">Data Entrega:</span>
<span className="font-medium text-slate-600 text-base">{formatDate(romaneio.data_prevista_entrega || '')}</span>
</div>
<div className="flex justify-between">
<span className="text-base">Prioridade:</span>
<span className={`font-medium px-3 py-1 rounded-full text-base ${
romaneio.prioridade === 'Urgente'
? 'bg-red-100 text-red-800'
: 'bg-green-100 text-green-800'
}`}>
{romaneio.prioridade}
</span>
</div>
</div>
</div>
<div className="break-inside-avoid-column">
<h3 className="font-semibold text-slate-800 mb-3 border-b pb-2 text-lg">Observações</h3>
<p className="mb-3 text-base">
<strong>Gerais:</strong> {romaneio.observacoes || 'N/A'}
</p>
<p className="mb-4 text-base">
<strong>Revisão:</strong> {romaneio.motivo_revisao || 'N/A'}
</p>
{/* Dados de transporte movidos para cá */}
<div className="mt-4 pt-3 border-t border-slate-200">
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-base">Revisão:</span>
<span className="font-medium text-slate-600 text-base">{romaneio.revisao}</span>
</div>
<div className="flex justify-between">
<span className="text-base">Motorista:</span>
<span className="font-medium text-slate-600 text-base">{romaneio.nome_motorista || 'N/A'}</span>
</div>
<div className="flex justify-between">
<span className="text-base">Transporte:</span>
<span className="font-medium text-slate-600 text-base">{formatTransporte(romaneio.tipo_transporte || '')}</span>
</div>
<div className="flex justify-between">
<span className="text-base">Frete:</span>
<span className="font-medium text-slate-600 text-base">{formatFrete(romaneio.frete_tipo || '')}</span>
</div>
<div className="flex justify-between">
<span className="text-base">Maior Dimensão:</span>
<span className="font-medium text-slate-600 text-base">{romaneio.maior_dimensao || 'N/A'}</span>
</div>
</div>
</div>
{discrepancia > 0 && (
<div className="p-3 bg-red-50 border border-red-200 rounded-md mt-4">
<p className="text-red-800 text-base">
<strong className="font-medium">Nota:</strong> Peso total ({pesoTotalGeral.toFixed(2)} Kg) superior à previsão ({previsaoKg.toFixed(2)} Kg).
</p>
</div>
)}
</div>
</div>
</section>
{/* Seção Lista de Peças - Expandida totalmente para ocupar largura */}
<section className="bg-white rounded-lg shadow-sm overflow-hidden -ml-8">
<h2 className="text-2xl font-semibold p-6">Lista de Peças</h2>
<div className="overflow-x-auto">
<table className="w-full text-base text-left text-slate-500">
<thead className="text-sm text-slate-700 uppercase bg-slate-100">
<tr>
<th scope="col" className="px-6 py-4">Marca</th>
<th scope="col" className="px-6 py-4">Fase</th>
<th scope="col" className="px-6 py-4">Descrição</th>
<th scope="col" className="px-6 py-4 text-right">Qtd.</th>
<th scope="col" className="px-6 py-4 text-right">Peso Unit. (Kg)</th>
<th scope="col" className="px-6 py-4 text-right">Peso Total (Kg)</th>
<th scope="col" className="px-4 py-4 text-center">Conf.Exp.</th>
<th scope="col" className="px-4 py-4 text-center">Conf.Obra</th>
</tr>
</thead>
<tbody>
{romaneio.itens_pecas && romaneio.itens_pecas.length > 0 ? (
romaneio.itens_pecas.map((item, index) => (
<tr key={index} className="bg-white border-b hover:bg-slate-50">
<td className="px-6 py-3 font-medium text-slate-900 whitespace-nowrap text-base">{item.marca}</td>
<td className="px-6 py-3 text-base">{item.fase || 'N/A'}</td>
<td className="px-6 py-3 text-base">{item.descricao || 'N/A'}</td>
<td className="px-6 py-3 text-right text-base">{item.quantidade_expedida}</td>
<td className="px-6 py-3 text-right text-base">{item.peso_unitario.toFixed(2)}</td>
<td className="px-6 py-3 text-right font-medium text-slate-800 text-base">{item.peso_total.toFixed(2)}</td>
<td className="px-4 py-3 text-center">
<input type="checkbox" className="h-5 w-5 rounded border-gray-300 text-sky-600" />
</td>
<td className="px-4 py-3 text-center">
<input type="checkbox" className="h-5 w-5 rounded border-gray-300 text-sky-600" />
</td>
</tr>
))
) : (
<tr>
<td colSpan={8} className="text-center py-8 text-slate-500 text-base">Nenhuma peça nesta lista.</td>
</tr>
)}
</tbody>
</table>
</div>
<div className="p-5 bg-slate-50 text-right font-bold">
<span className="text-slate-700 text-lg">Total Peças: <span>{pesoTotalPecas.toFixed(2)} Kg</span></span>
</div>
</section>
{/* Seção Lista de Insumos - Expandida totalmente para ocupar largura */}
<section className="bg-white rounded-lg shadow-sm overflow-hidden -ml-8">
<h2 className="text-2xl font-semibold p-6">Lista de Insumos</h2>
<div className="overflow-x-auto">
<table className="w-full text-base text-left text-slate-500">
<thead className="text-sm text-slate-700 uppercase bg-slate-100">
<tr>
<th scope="col" className="px-6 py-4">Tipo</th>
<th scope="col" className="px-6 py-4">Descrição</th>
<th scope="col" className="px-6 py-4 text-right">Quantidade</th>
<th scope="col" className="px-6 py-4">Unidade</th>
<th scope="col" className="px-6 py-4 text-right">Peso Unit. (Kg)</th>
<th scope="col" className="px-6 py-4 text-right">Peso Total (Kg)</th>
<th scope="col" className="px-6 py-4">Observações</th>
</tr>
</thead>
<tbody>
{romaneio.itens_insumos && romaneio.itens_insumos.length > 0 ? (
romaneio.itens_insumos.map((item, index) => (
<tr key={index} className="bg-white border-b hover:bg-slate-50">
<td className="px-6 py-3 font-medium text-slate-900 text-base">{item.tipo_insumo}</td>
<td className="px-6 py-3 text-base">{item.descricao}</td>
<td className="px-6 py-3 text-right text-base">{item.quantidade_expedida}</td>
<td className="px-6 py-3 text-base">{item.unidade}</td>
<td className="px-6 py-3 text-right text-base">{(item.peso_unitario || 0).toFixed(2)}</td>
<td className="px-6 py-3 text-right font-medium text-slate-800 text-base">{(item.peso_total || 0).toFixed(2)}</td>
<td className="px-6 py-3 text-base">{item.observacoes || 'N/A'}</td>
</tr>
))
) : (
<tr>
<td colSpan={7} className="text-center py-8 text-slate-500 text-base">Nenhum insumo nesta lista.</td>
</tr>
)}
</tbody>
</table>
</div>
</section>
{/* Seção de Assinaturas */}
<section className="mt-16 pt-8">
<div className="grid grid-cols-1 md:grid-cols-3 gap-16 text-slate-700 font-medium">
<p className="border-t-2 border-slate-300 pt-3 text-lg">Preparado por:</p>
<p className="border-t-2 border-slate-300 pt-3 text-lg">Recebido por:</p>
<p className="border-t-2 border-slate-300 pt-3 text-lg">Conferido por:</p>
</div>
</section>
</main>
</div>
</div>
</div>
);
};
@@ -0,0 +1,251 @@
import React from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { X, FileText } from 'lucide-react';
import { RomaneioExpedicao } from '@/hooks/useRomaneios';
import { formatBrazilianDateFromString } from '@/utils/dateTimeUtils';
import { generateProfessionalPDF } from '@/utils/pdfGenerator';
import { toast } from 'sonner';
interface RomaneioReportModalProps {
isOpen: boolean;
onClose: () => void;
romaneio: RomaneioExpedicao;
}
interface PecaAgrupada {
marca: string;
descricao: string;
quantidade: number;
peso_unitario: number;
peso_total: number;
}
export const RomaneioReportModal: React.FC<RomaneioReportModalProps> = ({
isOpen,
onClose,
romaneio
}) => {
const handleGeneratePDF = async () => {
try {
const filename = `romaneio-${romaneio.numero_romaneio}.pdf`;
await generateProfessionalPDF('romaneio-report-content', filename);
toast.success('PDF gerado com sucesso!');
} catch (error) {
console.error('Erro ao gerar PDF:', error);
toast.error('Erro ao gerar PDF. Tente novamente.');
}
};
// Função para formatar o tipo de transporte
const formatTransporte = (tipo: string) => {
const tipos: Record<string, string> = {
'carro': 'Carro',
'utilitario': 'Utilitário',
'caminho_pequeno': 'Caminhão Pequeno',
'caminhao_trucado': 'Caminhão Trucado',
'caminhao_munck': 'Caminhão Munck',
'carreta_12m': 'Carreta 12m',
'carreta_15m': 'Carreta 15m',
'especial': 'Especial'
};
return tipos[tipo] || tipo;
};
// Função para formatar o tipo de frete
const formatFrete = (tipo: string) => {
return tipo === 'proprio' ? 'Próprio' : 'Terceiros';
};
// Agrupar peças por fase e ordenar por marca
const pecasPorFase = React.useMemo(() => {
if (!romaneio.itens_pecas) return {};
const agrupadas: Record<string, PecaAgrupada[]> = {};
romaneio.itens_pecas.forEach(item => {
const fase = item.fase || 'Sem Fase';
if (!agrupadas[fase]) {
agrupadas[fase] = [];
}
agrupadas[fase].push({
marca: item.marca,
descricao: item.descricao || '',
quantidade: item.quantidade_expedida,
peso_unitario: item.peso_unitario,
peso_total: item.peso_total
});
});
// Ordenar peças por marca (numericamente se for número)
Object.keys(agrupadas).forEach(fase => {
agrupadas[fase].sort((a, b) => {
const marcaA = isNaN(Number(a.marca)) ? a.marca : Number(a.marca);
const marcaB = isNaN(Number(b.marca)) ? b.marca : Number(b.marca);
if (typeof marcaA === 'number' && typeof marcaB === 'number') {
return marcaA - marcaB;
}
return String(marcaA).localeCompare(String(marcaB));
});
});
return agrupadas;
}, [romaneio.itens_pecas]);
// Calcular totais
const totalPecas = romaneio.itens_pecas?.reduce((sum, item) => sum + item.quantidade_expedida, 0) || 0;
const pesoTotal = romaneio.peso_total_romaneio || 0;
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Relatório de Romaneio - {romaneio.numero_romaneio}</DialogTitle>
<button
onClick={onClose}
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
>
<X className="h-4 w-4" />
</button>
</DialogHeader>
<div className="bg-gray-100 p-6">
<div id="romaneio-report-content" className="max-w-5xl mx-auto bg-white rounded-lg shadow-lg p-8">
{/* Cabeçalho */}
<header className="pb-6 border-b border-gray-200">
<div className="flex justify-between items-start mb-6">
<div>
<h1 className="text-2xl font-bold text-gray-800">Romaneio de peças - OF {romaneio.of_number}</h1>
<p className="text-sm text-gray-500">{romaneio.numero_romaneio} - Revisão: {romaneio.revisao}</p>
</div>
<div className="text-right">
<Button
onClick={handleGeneratePDF}
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition duration-150"
>
<FileText className="w-4 h-4 mr-2" />
Gerar PDF
</Button>
</div>
</div>
<div className="grid grid-cols-3 gap-6 items-start">
{/* Coluna Esquerda: Dados do Romaneio */}
<div className="col-span-2 grid grid-cols-3 gap-x-6 gap-y-4">
<div>
<p className="text-xs font-medium text-gray-500">Data do Romaneio</p>
<p className="text-sm font-semibold text-gray-800">
{formatBrazilianDateFromString(romaneio.data_romaneio)}
</p>
</div>
<div>
<p className="text-xs font-medium text-gray-500">Data Prev. Entrega</p>
<p className="text-sm font-semibold text-gray-800">
{romaneio.data_prevista_entrega ? formatBrazilianDateFromString(romaneio.data_prevista_entrega) : 'N/A'}
</p>
</div>
<div className="bg-yellow-50 border border-yellow-300 rounded-lg p-3 text-center">
<p className="text-xs font-medium text-yellow-800">Maior Dimensão</p>
<p className="text-sm font-semibold text-lg text-yellow-900">
{romaneio.maior_dimensao || 'N/A'}
</p>
</div>
<div>
<p className="text-xs font-medium text-gray-500">Tipo de Transporte</p>
<p className="text-sm font-semibold text-gray-800">
{formatTransporte(romaneio.tipo_transporte || '')}
</p>
</div>
<div>
<p className="text-xs font-medium text-gray-500">Tipo de Frete</p>
<p className="text-sm font-semibold text-gray-800">
{formatFrete(romaneio.frete_tipo || '')}
</p>
</div>
<div>
<p className="text-xs font-medium text-gray-500">Nome do Motorista</p>
<p className="text-sm font-semibold text-gray-800">
{romaneio.nome_motorista || 'N/A'}
</p>
</div>
</div>
{/* Coluna Direita: Resumo Total - Reduzido em 30% */}
<div className="col-span-1 space-y-3 scale-75">
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3 text-center">
<p className="text-xs font-medium text-blue-700">Quantidade Total de Peças</p>
<p className="text-xl font-bold text-blue-900">{totalPecas}</p>
</div>
<div className="bg-green-50 border border-green-200 rounded-lg p-3 text-center">
<p className="text-xs font-medium text-green-700">Peso Total</p>
<p className="text-xl font-bold text-green-900">{pesoTotal.toFixed(2)} kg</p>
</div>
</div>
</div>
</header>
{/* Itens do Romaneio Agrupados por Fase */}
<div className="space-y-6 mt-6">
{Object.entries(pecasPorFase)
.sort(([a], [b]) => {
// Ordenar fases numericamente se possível
const faseA = a.toLowerCase().replace('fase', '').trim();
const faseB = b.toLowerCase().replace('fase', '').trim();
if (!isNaN(Number(faseA)) && !isNaN(Number(faseB))) {
return Number(faseA) - Number(faseB);
}
return a.localeCompare(b);
})
.map(([fase, pecas]) => (
<div key={fase}>
<h2 className="text-xl font-semibold text-gray-700 pb-2 border-b-2 border-gray-300 mb-4">
{fase === 'Sem Fase' ? 'Sem Fase' : `Fase: ${fase}`}
</h2>
<div className="overflow-x-auto">
<table className="min-w-full bg-white">
<thead className="bg-gray-50">
<tr>
<th className="py-1 px-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Marca</th>
<th className="py-1 px-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Descrição</th>
<th className="py-1 px-4 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Qtd</th>
<th className="py-1 px-4 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Peso Unit.</th>
<th className="py-1 px-4 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Peso Total</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{pecas.map((peca, index) => (
<tr key={index}>
<td className="py-1 px-4 whitespace-nowrap text-sm font-medium text-gray-900">
{peca.marca}
</td>
<td className="py-1 px-4 whitespace-nowrap text-sm text-gray-600">
{peca.descricao || 'N/A'}
</td>
<td className="py-1 px-4 whitespace-nowrap text-sm text-gray-600 text-center">
{peca.quantidade}
</td>
<td className="py-1 px-4 whitespace-nowrap text-sm text-gray-600 text-right">
{peca.peso_unitario.toFixed(0)} kg
</td>
<td className="py-1 px-4 whitespace-nowrap text-sm font-semibold text-gray-800 text-right">
{peca.peso_total.toFixed(0)} kg
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
))}
</div>
</div>
</div>
</DialogContent>
</Dialog>
);
};
+180
View File
@@ -0,0 +1,180 @@
import React, { useState } from 'react';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Edit, Eye, Trash2, Printer } from 'lucide-react';
import { RomaneioExpedicao } from '@/hooks/useRomaneios';
import { formatBrazilianDateFromString } from '@/utils/dateTimeUtils';
import { RomaneioReportModal } from './RomaneioReportModal';
interface RomaneioTableProps {
romaneios: RomaneioExpedicao[];
onEdit: (romaneio: RomaneioExpedicao) => void;
onView: (romaneio: RomaneioExpedicao) => void;
onDelete: (id: string) => void;
onPrint: (romaneio: RomaneioExpedicao) => void;
loading?: boolean;
}
export const RomaneioTable: React.FC<RomaneioTableProps> = ({
romaneios,
onEdit,
onView,
onDelete,
onPrint,
loading = false
}) => {
const [selectedRomaneioForReport, setSelectedRomaneioForReport] = useState<RomaneioExpedicao | null>(null);
const [showReportModal, setShowReportModal] = useState(false);
const handlePrintReport = (romaneio: RomaneioExpedicao) => {
setSelectedRomaneioForReport(romaneio);
setShowReportModal(true);
};
const handleCloseReportModal = () => {
setShowReportModal(false);
setSelectedRomaneioForReport(null);
};
const getStatusColor = (status: string) => {
switch (status) {
case 'Em planejamento':
return 'bg-orange-100 text-orange-800';
case 'Confirmado':
return 'bg-blue-100 text-blue-800';
case 'Entregue':
return 'bg-blue-100 text-blue-800';
case 'Conferido em Obra':
return 'bg-green-100 text-green-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
const getPriorityColor = (prioridade: string) => {
switch (prioridade) {
case 'Urgente':
return 'bg-red-100 text-red-800';
case 'Normal':
return 'bg-green-100 text-green-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
if (loading) {
return (
<div className="animate-pulse space-y-4">
<div className="h-8 bg-muted rounded w-full"></div>
{[...Array(5)].map((_, i) => (
<div key={i} className="h-16 bg-muted rounded w-full"></div>
))}
</div>
);
}
return (
<>
<div className="w-full overflow-x-auto">
<Table disableOverflow>
<TableHeader>
<TableRow>
<TableHead className="w-[120px]">Número</TableHead>
<TableHead className="w-[100px]">OF</TableHead>
<TableHead className="w-[110px]">Data Romaneio</TableHead>
<TableHead className="w-[110px]">Data Entrega</TableHead>
<TableHead className="w-[100px]">Status</TableHead>
<TableHead className="w-[100px]">Prioridade</TableHead>
<TableHead className="w-[120px]">Transporte</TableHead>
<TableHead className="w-[100px] text-right">Peso Total</TableHead>
<TableHead className="w-[120px]">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{romaneios.map((romaneio) => (
<TableRow key={romaneio.id}>
<TableCell className="font-medium">
{romaneio.numero_romaneio}
</TableCell>
<TableCell>{romaneio.of_number}</TableCell>
<TableCell>
{formatBrazilianDateFromString(romaneio.data_romaneio)}
</TableCell>
<TableCell>
{romaneio.data_prevista_entrega
? formatBrazilianDateFromString(romaneio.data_prevista_entrega)
: 'N/A'
}
</TableCell>
<TableCell>
<Badge className={`text-xs ${getStatusColor(romaneio.status)}`}>
{romaneio.status === 'Confirmado' ? 'Entregue' : romaneio.status}
</Badge>
</TableCell>
<TableCell>
<Badge className={`text-xs ${getPriorityColor(romaneio.prioridade)}`}>
{romaneio.prioridade}
</Badge>
</TableCell>
<TableCell>
{romaneio.tipo_transporte || 'N/A'}
</TableCell>
<TableCell className="text-right font-medium">
{romaneio.peso_total_romaneio?.toFixed(2) || '0.00'} kg
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => onView(romaneio)}
className="h-8 w-8 p-0"
>
<Eye className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => onEdit(romaneio)}
className="h-8 w-8 p-0"
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handlePrintReport(romaneio)}
className="h-8 w-8 p-0"
>
<Printer className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => onDelete(romaneio.id)}
className="h-8 w-8 p-0 text-red-600 hover:text-red-700"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{/* Modal de Relatório */}
{selectedRomaneioForReport && (
<RomaneioReportModal
isOpen={showReportModal}
onClose={handleCloseReportModal}
romaneio={selectedRomaneioForReport}
/>
)}
</>
);
};