🚀 Initial commit: Versão atual do TrackSteel APP
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user