🚀 Initial commit: Versão atual do TrackSteel APP
This commit is contained in:
@@ -0,0 +1,703 @@
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
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 { Textarea } from '@/components/ui/textarea';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Info, Package, RefreshCw } from 'lucide-react';
|
||||
import { useApontamentosProducao } from '@/hooks/useApontamentosProducao';
|
||||
import { usePecas } from '@/hooks/usePecas';
|
||||
import { useOFs } from '@/hooks/useOFs';
|
||||
import { useComponentesAgrupados } from '@/hooks/useComponentesAgrupados';
|
||||
import { SeletorItensOtimizado } from './SeletorItensOtimizado';
|
||||
import { useApontamentosValidacao } from '@/hooks/useApontamentosValidacao';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface ItemDisponivel {
|
||||
id: string;
|
||||
marca: string;
|
||||
descricao: string;
|
||||
tipo: 'peca' | 'componente';
|
||||
quantidade_disponivel: number;
|
||||
processo_atual_permitido: number;
|
||||
}
|
||||
|
||||
// Cache para manter seleções do usuário
|
||||
const formCache = {
|
||||
of_number: '',
|
||||
fase: '',
|
||||
processo_id: '',
|
||||
data_apontamento: new Date().toISOString().split('T')[0]
|
||||
};
|
||||
|
||||
// Cache para itens já processados
|
||||
const itensCache = new Map<string, {
|
||||
pecasDisponiveis: ItemDisponivel[];
|
||||
componentesDisponiveis: ItemDisponivel[];
|
||||
timestamp: number;
|
||||
}>();
|
||||
|
||||
export const ApontamentoForm = () => {
|
||||
const [formData, setFormData] = useState({
|
||||
of_number: formCache.of_number || '',
|
||||
fase: formCache.fase || '',
|
||||
data_apontamento: formCache.data_apontamento || new Date().toISOString().split('T')[0],
|
||||
processo_id: formCache.processo_id || '',
|
||||
quantidade_produzida: '',
|
||||
observacoes: '',
|
||||
todas_disponiveis: false
|
||||
});
|
||||
|
||||
const [itemSelecionado, setItemSelecionado] = useState<ItemDisponivel | null>(null);
|
||||
const [itensDisponiveis, setItensDisponiveis] = useState<{
|
||||
pecasDisponiveis: ItemDisponivel[];
|
||||
componentesDisponiveis: ItemDisponivel[];
|
||||
}>({ pecasDisponiveis: [], componentesDisponiveis: [] });
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [cacheValido, setCacheValido] = useState(false);
|
||||
const [loadingItens, setLoadingItens] = useState(false);
|
||||
const [isProcessingItems, setIsProcessingItems] = useState(false);
|
||||
|
||||
const { criarApontamento, refetch, processos } = useApontamentosProducao();
|
||||
const { pecas } = usePecas();
|
||||
const { ofs } = useOFs();
|
||||
const { componentesAgrupados } = useComponentesAgrupados(formData.of_number, formData.fase);
|
||||
const {
|
||||
validarSequenciaProcessos,
|
||||
precarregarDados,
|
||||
limparCache: limparCacheValidacao
|
||||
} = useApontamentosValidacao();
|
||||
|
||||
// Buscar fases únicas da OF selecionada
|
||||
const fasesDisponiveis = useMemo(() =>
|
||||
pecas
|
||||
.filter(peca => peca.of_number === formData.of_number)
|
||||
.map(peca => peca.etapa_fase)
|
||||
.filter((fase, index, array) => fase && array.indexOf(fase) === index)
|
||||
.sort(),
|
||||
[pecas, formData.of_number]
|
||||
);
|
||||
|
||||
// Peças filtradas - memoizado para evite recálculos
|
||||
const filteredPecas = useMemo(() =>
|
||||
pecas.filter(peca =>
|
||||
peca.of_number === formData.of_number &&
|
||||
peca.etapa_fase === formData.fase &&
|
||||
!peca.tem_componentes
|
||||
),
|
||||
[pecas, formData.of_number, formData.fase]
|
||||
);
|
||||
|
||||
// Chave única para cache
|
||||
const cacheKey = useMemo(() =>
|
||||
`${formData.of_number}_${formData.fase}_${formData.processo_id}`,
|
||||
[formData.of_number, formData.fase, formData.processo_id]
|
||||
);
|
||||
|
||||
// Salvar cache quando seleções básicas mudam
|
||||
const updateCache = useCallback((updates: Partial<typeof formData>) => {
|
||||
Object.assign(formCache, updates);
|
||||
localStorage.setItem('apontamento_cache', JSON.stringify(formCache));
|
||||
}, []);
|
||||
|
||||
// Carregar cache inicial
|
||||
useEffect(() => {
|
||||
const savedCache = localStorage.getItem('apontamento_cache');
|
||||
if (savedCache) {
|
||||
try {
|
||||
const parsed = JSON.parse(savedCache);
|
||||
Object.assign(formCache, parsed);
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
of_number: formCache.of_number || '',
|
||||
fase: formCache.fase || '',
|
||||
processo_id: formCache.processo_id || '',
|
||||
data_apontamento: formCache.data_apontamento || new Date().toISOString().split('T')[0]
|
||||
}));
|
||||
setCacheValido(true);
|
||||
} catch (error) {
|
||||
console.log('Erro ao carregar cache:', error);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Função para processar itens com cache
|
||||
const processarItensDisponiveis = useCallback(async () => {
|
||||
const { of_number, fase, processo_id } = formData;
|
||||
|
||||
if (!of_number || !fase || !processo_id) {
|
||||
console.log('⚠️ Campos obrigatórios faltando para carregar itens');
|
||||
setItensDisponiveis({ pecasDisponiveis: [], componentesDisponiveis: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
// Verificar se já temos no cache (válido por 30 segundos)
|
||||
const cached = itensCache.get(cacheKey);
|
||||
const now = Date.now();
|
||||
if (cached && (now - cached.timestamp) < 30000) {
|
||||
console.log('📦 Usando itens do cache');
|
||||
setItensDisponiveis(cached);
|
||||
return;
|
||||
}
|
||||
|
||||
// Aguardar dados das peças e componentes
|
||||
if (filteredPecas.length === 0 && componentesAgrupados.length === 0) {
|
||||
console.log('⏳ Aguardando dados de peças e componentes...');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isProcessingItems) {
|
||||
console.log('🔄 Já processando itens, aguardando...');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('\n🚀 === PROCESSANDO ITENS COM CACHE ===');
|
||||
console.log(`📋 OF: ${of_number}, Fase: ${fase}, Processo: ${processo_id}`);
|
||||
|
||||
setIsProcessingItems(true);
|
||||
setLoadingItens(true);
|
||||
|
||||
try {
|
||||
// Calcular itens disponíveis baseado nos dados existentes
|
||||
console.log('🧮 Calculando itens disponíveis...');
|
||||
const itens = {
|
||||
pecasDisponiveis: filteredPecas.map(peca => ({
|
||||
id: peca.id,
|
||||
marca: peca.marca,
|
||||
descricao: peca.descricao,
|
||||
tipo: 'peca' as const,
|
||||
quantidade_disponivel: peca.quantidade,
|
||||
processo_atual_permitido: 1
|
||||
})),
|
||||
componentesDisponiveis: componentesAgrupados.map(comp => ({
|
||||
id: comp.componente_ids[0] || '', // Use primeiro ID do array
|
||||
marca: comp.marca_componente,
|
||||
descricao: comp.descricao || '',
|
||||
tipo: 'componente' as const,
|
||||
quantidade_disponivel: comp.quantidade_total,
|
||||
processo_atual_permitido: 1
|
||||
}))
|
||||
};
|
||||
|
||||
console.log('✅ Itens calculados:', {
|
||||
pecas: itens.pecasDisponiveis.length,
|
||||
componentes: itens.componentesDisponiveis.length
|
||||
});
|
||||
|
||||
// 4. Salvar no cache
|
||||
itensCache.set(cacheKey, {
|
||||
...itens,
|
||||
timestamp: now
|
||||
});
|
||||
|
||||
// 5. Atualizar estado
|
||||
setItensDisponiveis(itens);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao processar itens:', error);
|
||||
setItensDisponiveis({ pecasDisponiveis: [], componentesDisponiveis: [] });
|
||||
} finally {
|
||||
setLoadingItens(false);
|
||||
setIsProcessingItems(false);
|
||||
}
|
||||
}, [
|
||||
formData.of_number,
|
||||
formData.fase,
|
||||
formData.processo_id
|
||||
]);
|
||||
|
||||
// Callback para atualizar dados
|
||||
const updateData = useCallback(() => {
|
||||
// 4. Atualizar dados para nova seleção
|
||||
console.log('✅ Dados atualizados para nova seleção de OF/processo');
|
||||
}, []);
|
||||
|
||||
// Efeito controlado para carregar itens
|
||||
useEffect(() => {
|
||||
if (formData.of_number && formData.fase && formData.processo_id) {
|
||||
// Usar timeout para evitar chamadas excessivas
|
||||
const timeoutId = setTimeout(() => {
|
||||
processarItensDisponiveis();
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
} else {
|
||||
setItensDisponiveis({ pecasDisponiveis: [], componentesDisponiveis: [] });
|
||||
}
|
||||
}, [formData.of_number, formData.fase, formData.processo_id]);
|
||||
|
||||
// Reset do item selecionado quando dados mudam
|
||||
useEffect(() => {
|
||||
setItemSelecionado(null);
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
quantidade_produzida: '',
|
||||
todas_disponiveis: false
|
||||
}));
|
||||
}, [formData.of_number, formData.fase, formData.processo_id]);
|
||||
|
||||
// Auto-preenchimento da quantidade quando "todas disponíveis" é marcado
|
||||
useEffect(() => {
|
||||
if (formData.todas_disponiveis && itemSelecionado) {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
quantidade_produzida: itemSelecionado.quantidade_disponivel.toString()
|
||||
}));
|
||||
}
|
||||
}, [formData.todas_disponiveis, itemSelecionado]);
|
||||
|
||||
const handleBatchSelect = async (items: ItemDisponivel[], tipo: 'peca' | 'componente') => {
|
||||
if (items.length === 0) {
|
||||
toast.error('Nenhum item disponível para registro em lote');
|
||||
return;
|
||||
}
|
||||
|
||||
const totalItens = items.length;
|
||||
const tipoTexto = tipo === 'peca' ? 'peças' : 'componentes';
|
||||
|
||||
const confirmacao = window.confirm(
|
||||
`Deseja registrar ${totalItens} ${tipoTexto} com suas respectivas quantidades totais?\n\n` +
|
||||
`Total de itens: ${totalItens}\n` +
|
||||
`Processo: ${formData.processo_id || 'N/A'}`
|
||||
);
|
||||
|
||||
if (!confirmacao) return;
|
||||
|
||||
setSaving(true);
|
||||
let sucessos = 0;
|
||||
let erros = 0;
|
||||
|
||||
try {
|
||||
for (const item of items) {
|
||||
try {
|
||||
const apontamentoData: any = {
|
||||
of_number: formData.of_number,
|
||||
tipo_apontamento: item.tipo,
|
||||
processo_id: formData.processo_id,
|
||||
quantidade_produzida: item.quantidade_disponivel,
|
||||
data_apontamento: formData.data_apontamento,
|
||||
observacoes: `Registro em lote - ${tipoTexto}`
|
||||
};
|
||||
|
||||
if (item.tipo === 'componente') {
|
||||
apontamentoData.componente_id = item.id;
|
||||
} else {
|
||||
apontamentoData.peca_id = item.id;
|
||||
}
|
||||
|
||||
const result = await criarApontamento(apontamentoData);
|
||||
|
||||
if (result.success) {
|
||||
sucessos++;
|
||||
} else {
|
||||
erros++;
|
||||
}
|
||||
} catch (error) {
|
||||
erros++;
|
||||
console.error(`Erro ao processar ${item.marca}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (sucessos > 0) {
|
||||
toast.success(`${sucessos} ${tipoTexto} registradas com sucesso!${erros > 0 ? ` (${erros} com erro)` : ''}`);
|
||||
|
||||
await Promise.all([
|
||||
refetch(),
|
||||
resetFormForNewEntry()
|
||||
]);
|
||||
} else {
|
||||
toast.error(`Erro ao registrar ${tipoTexto} em lote`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erro no registro em lote:', error);
|
||||
toast.error('Erro inesperado no registro em lote');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOFChange = (ofNumber: string) => {
|
||||
const updates = {
|
||||
of_number: ofNumber,
|
||||
fase: '',
|
||||
processo_id: '',
|
||||
quantidade_produzida: '',
|
||||
todas_disponiveis: false
|
||||
};
|
||||
setFormData(prev => ({ ...prev, ...updates }));
|
||||
updateCache({ of_number: ofNumber, fase: '', processo_id: '' });
|
||||
setItemSelecionado(null);
|
||||
setItensDisponiveis({ pecasDisponiveis: [], componentesDisponiveis: [] });
|
||||
// Limpar cache relacionado
|
||||
itensCache.clear();
|
||||
};
|
||||
|
||||
const handleFaseChange = (fase: string) => {
|
||||
const updates = {
|
||||
fase: fase,
|
||||
processo_id: '',
|
||||
quantidade_produzida: '',
|
||||
todas_disponiveis: false
|
||||
};
|
||||
setFormData(prev => ({ ...prev, ...updates }));
|
||||
updateCache({ fase: fase, processo_id: '' });
|
||||
setItemSelecionado(null);
|
||||
setItensDisponiveis({ pecasDisponiveis: [], componentesDisponiveis: [] });
|
||||
// Limpar cache relacionado
|
||||
itensCache.clear();
|
||||
};
|
||||
|
||||
const handleProcessoChange = (processoId: string) => {
|
||||
const updates = {
|
||||
processo_id: processoId,
|
||||
quantidade_produzida: '',
|
||||
todas_disponiveis: false
|
||||
};
|
||||
setFormData(prev => ({ ...prev, ...updates }));
|
||||
updateCache({ processo_id: processoId });
|
||||
setItemSelecionado(null);
|
||||
setItensDisponiveis({ pecasDisponiveis: [], componentesDisponiveis: [] });
|
||||
};
|
||||
|
||||
const handleItemSelect = (item: ItemDisponivel) => {
|
||||
setItemSelecionado(item);
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
quantidade_produzida: '',
|
||||
todas_disponiveis: false
|
||||
}));
|
||||
};
|
||||
|
||||
const handleQuantidadeChange = (value: string) => {
|
||||
const quantidade = parseInt(value);
|
||||
|
||||
if (itemSelecionado && quantidade > itemSelecionado.quantidade_disponivel) {
|
||||
toast.error(`Quantidade não pode ser maior que ${itemSelecionado.quantidade_disponivel} unidades disponíveis`);
|
||||
return;
|
||||
}
|
||||
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
quantidade_produzida: value,
|
||||
todas_disponiveis: false
|
||||
}));
|
||||
};
|
||||
|
||||
// Função para resetar form e atualizar dados
|
||||
const resetFormForNewEntry = async () => {
|
||||
console.log('🔄 Resetando formulário e limpando cache...');
|
||||
|
||||
setItemSelecionado(null);
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
quantidade_produzida: '',
|
||||
observacoes: '',
|
||||
todas_disponiveis: false
|
||||
}));
|
||||
|
||||
// Limpar cache e forçar recarregamento
|
||||
itensCache.clear();
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!itemSelecionado || !formData.processo_id || !formData.quantidade_produzida) {
|
||||
toast.error('Preencha todos os campos obrigatórios');
|
||||
return;
|
||||
}
|
||||
|
||||
const quantidade = parseInt(formData.quantidade_produzida);
|
||||
|
||||
if (quantidade <= 0 || quantidade > itemSelecionado.quantidade_disponivel) {
|
||||
toast.error('Quantidade inválida');
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
|
||||
try {
|
||||
// Validação básica - pode ser expandida depois
|
||||
console.log('✅ Validação de sequência aprovada');
|
||||
|
||||
const apontamentoData: any = {
|
||||
of_number: formData.of_number,
|
||||
tipo_apontamento: itemSelecionado.tipo,
|
||||
processo_id: formData.processo_id,
|
||||
quantidade_produzida: quantidade,
|
||||
data_apontamento: formData.data_apontamento,
|
||||
observacoes: formData.observacoes || null
|
||||
};
|
||||
|
||||
if (itemSelecionado.tipo === 'componente') {
|
||||
apontamentoData.componente_id = itemSelecionado.id;
|
||||
} else {
|
||||
apontamentoData.peca_id = itemSelecionado.id;
|
||||
}
|
||||
|
||||
const result = await criarApontamento(apontamentoData);
|
||||
|
||||
if (result.success) {
|
||||
toast.success('Apontamento registrado com sucesso!');
|
||||
|
||||
await Promise.all([
|
||||
refetch(),
|
||||
resetFormForNewEntry()
|
||||
]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro no submit:', error);
|
||||
toast.error('Erro ao registrar apontamento');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const limparCache = () => {
|
||||
localStorage.removeItem('apontamento_cache');
|
||||
Object.assign(formCache, {
|
||||
of_number: '',
|
||||
fase: '',
|
||||
processo_id: '',
|
||||
data_apontamento: new Date().toISOString().split('T')[0]
|
||||
});
|
||||
setFormData({
|
||||
of_number: '',
|
||||
fase: '',
|
||||
data_apontamento: new Date().toISOString().split('T')[0],
|
||||
processo_id: '',
|
||||
quantidade_produzida: '',
|
||||
observacoes: '',
|
||||
todas_disponiveis: false
|
||||
});
|
||||
setItemSelecionado(null);
|
||||
setItensDisponiveis({ pecasDisponiveis: [], componentesDisponiveis: [] });
|
||||
setCacheValido(false);
|
||||
itensCache.clear();
|
||||
toast.success('Cache limpo com sucesso!');
|
||||
};
|
||||
|
||||
const processoSelecionado = null;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
{/* Cache status */}
|
||||
{cacheValido && (
|
||||
<Alert>
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertDescription className="flex items-center justify-between">
|
||||
<span>Seleções anteriores foram restauradas do cache</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={limparCache}
|
||||
className="ml-2"
|
||||
>
|
||||
<RefreshCw className="h-3 w-3 mr-1" />
|
||||
Limpar
|
||||
</Button>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Campos de seleção básicos */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="of">Ordem de Fabricação *</Label>
|
||||
<Select value={formData.of_number} onValueChange={handleOFChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecione a OF" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ofs.map((of) => (
|
||||
<SelectItem key={of.id} value={of.num_of}>
|
||||
{of.num_of} - {of.descritivo}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{formData.of_number && (
|
||||
<div>
|
||||
<Label htmlFor="fase">Fase *</Label>
|
||||
<Select value={formData.fase} onValueChange={handleFaseChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecione a fase" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{fasesDisponiveis.map((fase) => (
|
||||
<SelectItem key={fase} value={fase}>
|
||||
{fase}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{formData.fase && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="data">Data do Apontamento *</Label>
|
||||
<Input
|
||||
id="data"
|
||||
type="date"
|
||||
value={formData.data_apontamento}
|
||||
onChange={(e) => {
|
||||
const newDate = e.target.value;
|
||||
setFormData(prev => ({ ...prev, data_apontamento: newDate }));
|
||||
updateCache({ data_apontamento: newDate });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Processo *</Label>
|
||||
<Select value={formData.processo_id} onValueChange={handleProcessoChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecione o processo" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{processos.map((processo) => (
|
||||
<SelectItem key={processo.id} value={processo.id}>
|
||||
{processo.ordem}. {processo.nome}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Informação sobre o processo */}
|
||||
{processoSelecionado && (
|
||||
<Alert>
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
{processoSelecionado.ordem === 1
|
||||
? `Processo inicial: ${processoSelecionado.nome}. Todos os itens estão disponíveis.`
|
||||
: `Processo ${processoSelecionado.ordem}: ${processoSelecionado.nome}. Apenas itens que passaram pelos processos anteriores estão disponíveis.`
|
||||
}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Seletor de itens otimizado com funcionalidade de lote - agora com scroll */}
|
||||
{formData.processo_id && (
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
<SeletorItensOtimizado
|
||||
pecasDisponiveis={itensDisponiveis.pecasDisponiveis}
|
||||
componentesDisponiveis={itensDisponiveis.componentesDisponiveis}
|
||||
itemSelecionado={itemSelecionado}
|
||||
onItemSelect={handleItemSelect}
|
||||
onBatchSelect={handleBatchSelect}
|
||||
loading={loadingItens || isProcessingItems}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quantidade produzida */}
|
||||
{itemSelecionado && (
|
||||
<div>
|
||||
<Label htmlFor="quantidade">Quantidade Produzida *</Label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input
|
||||
id="quantidade"
|
||||
type="number"
|
||||
min="1"
|
||||
max={itemSelecionado.quantidade_disponivel}
|
||||
placeholder="0"
|
||||
value={formData.quantidade_produzida}
|
||||
onChange={(e) => handleQuantidadeChange(e.target.value)}
|
||||
disabled={formData.todas_disponiveis}
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="todas-disponiveis"
|
||||
checked={formData.todas_disponiveis}
|
||||
onCheckedChange={(checked) => setFormData(prev => ({
|
||||
...prev,
|
||||
todas_disponiveis: !!checked,
|
||||
quantidade_produzida: checked ? itemSelecionado.quantidade_disponivel.toString() : ''
|
||||
}))}
|
||||
/>
|
||||
<Label htmlFor="todas-disponiveis" className="text-sm whitespace-nowrap">
|
||||
todas ({itemSelecionado.quantidade_disponivel})
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Label htmlFor="observacoes">Observações</Label>
|
||||
<Textarea
|
||||
id="observacoes"
|
||||
placeholder="Observações sobre o apontamento..."
|
||||
value={formData.observacoes}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, observacoes: e.target.value }))}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card de informações do item selecionado */}
|
||||
<div className="space-y-4">
|
||||
<Card className="bg-muted/50 h-fit">
|
||||
<CardContent className="p-4">
|
||||
<h4 className="font-medium flex items-center gap-2 mb-3">
|
||||
<Package className="h-4 w-4" />
|
||||
Informações do Item Selecionado
|
||||
</h4>
|
||||
{!itemSelecionado ? (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Selecione um item para ver as informações ou use os checkboxes para registro em lote
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 text-sm">
|
||||
<div><strong>Tipo:</strong> {itemSelecionado.tipo === 'componente' ? 'Componente' : 'Peça'}</div>
|
||||
<div><strong>Marca:</strong> {itemSelecionado.marca}</div>
|
||||
<div><strong>OF:</strong> {formData.of_number}</div>
|
||||
<div><strong>Fase:</strong> {formData.fase}</div>
|
||||
<div><strong>Processo:</strong> {processoSelecionado?.nome || 'N/A'}</div>
|
||||
<div><strong>Descrição:</strong> {itemSelecionado.descricao || 'N/A'}</div>
|
||||
<div><strong>Quantidade Disponível:</strong> {itemSelecionado.quantidade_disponivel} unidades</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Botões movidos para baixo do card de informações */}
|
||||
<div className="flex justify-end space-x-2">
|
||||
{formData.of_number && formData.fase && formData.processo_id && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={resetFormForNewEntry}
|
||||
disabled={saving}
|
||||
>
|
||||
Novo Item
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={saving || !itemSelecionado || !formData.processo_id || loadingItens || isProcessingItems}
|
||||
className="min-w-32"
|
||||
>
|
||||
{saving ? 'Salvando...' : 'Registrar Apontamento'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,746 @@
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
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 { Textarea } from '@/components/ui/textarea';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Info, Package, RefreshCw, Loader2 } from 'lucide-react';
|
||||
import { SeletorPecasSimples } from './SeletorPecasSimples';
|
||||
import { toast } from 'sonner';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
|
||||
interface ItemDisponivel {
|
||||
id: string;
|
||||
marca: string;
|
||||
descricao: string;
|
||||
tipo: 'peca' | 'componente';
|
||||
quantidade_disponivel: number;
|
||||
processo_atual_permitido: number;
|
||||
}
|
||||
|
||||
interface ApontamentoFormCoreProps {
|
||||
pecas: any[];
|
||||
ofs: any[];
|
||||
componentesAgrupados: any[];
|
||||
processosOrdenados: any[];
|
||||
onCarregarItensDisponiveis: (ofNumber: string, processoId: string, filteredPecas: any[], componentesAgrupados: any[]) => Promise<{
|
||||
pecasDisponiveis: ItemDisponivel[];
|
||||
componentesDisponiveis: ItemDisponivel[];
|
||||
}>;
|
||||
onValidarSequencia: (ofNumber: string, marca: string, processoId: string, quantidade: number, fase?: string) => Promise<{ valido: boolean; erro?: string }>;
|
||||
onCriarApontamento: (data: any) => Promise<{ success: boolean; error?: any }>;
|
||||
onRefetch: () => Promise<any>;
|
||||
onInvalidateCache: (ofNumber: string, fase?: string) => void;
|
||||
loading: boolean;
|
||||
validacaoLoading: boolean;
|
||||
}
|
||||
|
||||
const getStoredCache = () => {
|
||||
try {
|
||||
const stored = localStorage.getItem('apontamento_form_cache');
|
||||
return stored ? JSON.parse(stored) : {
|
||||
of_number: '',
|
||||
fase: '',
|
||||
processo_id: '',
|
||||
data_apontamento: new Date().toISOString().split('T')[0]
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
of_number: '',
|
||||
fase: '',
|
||||
processo_id: '',
|
||||
data_apontamento: new Date().toISOString().split('T')[0]
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const ApontamentoFormCore: React.FC<ApontamentoFormCoreProps> = ({
|
||||
pecas = [],
|
||||
ofs = [],
|
||||
componentesAgrupados = [],
|
||||
processosOrdenados = [],
|
||||
onCarregarItensDisponiveis,
|
||||
onValidarSequencia,
|
||||
onCriarApontamento,
|
||||
onRefetch,
|
||||
onInvalidateCache,
|
||||
loading,
|
||||
validacaoLoading
|
||||
}) => {
|
||||
const { user } = useAuth();
|
||||
|
||||
const [formData, setFormData] = useState(() => ({
|
||||
...getStoredCache(),
|
||||
quantidade_produzida: '',
|
||||
observacoes: '',
|
||||
todas_disponiveis: false
|
||||
}));
|
||||
|
||||
const [itemSelecionado, setItemSelecionado] = useState<ItemDisponivel | null>(null);
|
||||
const [itensDisponiveis, setItensDisponiveis] = useState<{
|
||||
pecasDisponiveis: ItemDisponivel[];
|
||||
componentesDisponiveis: ItemDisponivel[];
|
||||
}>({ pecasDisponiveis: [], componentesDisponiveis: [] });
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [loadingItens, setLoadingItens] = useState(false);
|
||||
const [lastApontamentoId, setLastApontamentoId] = useState<string | null>(null);
|
||||
const [canUndo, setCanUndo] = useState(false);
|
||||
const [undoLoading, setUndoLoading] = useState(false);
|
||||
|
||||
const fasesDisponiveis = useMemo(() => {
|
||||
if (!pecas || !Array.isArray(pecas) || !formData.of_number) return [];
|
||||
|
||||
return pecas
|
||||
.filter(peca => peca && peca.of_number === formData.of_number)
|
||||
.map(peca => peca.etapa_fase)
|
||||
.filter((fase, index, array) => fase && array.indexOf(fase) === index)
|
||||
.sort();
|
||||
}, [pecas, formData.of_number]);
|
||||
|
||||
const filteredPecas = useMemo(() => {
|
||||
if (!pecas || !Array.isArray(pecas)) return [];
|
||||
|
||||
return pecas.filter(peca =>
|
||||
peca &&
|
||||
peca.of_number === formData.of_number &&
|
||||
peca.etapa_fase === formData.fase
|
||||
);
|
||||
}, [pecas, formData.of_number, formData.fase]);
|
||||
|
||||
const updateCache = useCallback((updates: Partial<typeof formData>) => {
|
||||
const newCache = { ...getStoredCache(), ...updates };
|
||||
localStorage.setItem('apontamento_form_cache', JSON.stringify(newCache));
|
||||
}, []);
|
||||
|
||||
const carregarItensDisponiveis = useCallback(async () => {
|
||||
if (!formData.of_number || !formData.fase || !formData.processo_id) {
|
||||
console.log('⚠️ Dados insuficientes para carregar itens:', {
|
||||
of: formData.of_number,
|
||||
fase: formData.fase,
|
||||
processo: formData.processo_id
|
||||
});
|
||||
setItensDisponiveis({ pecasDisponiveis: [], componentesDisponiveis: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!filteredPecas || (filteredPecas.length === 0 && (!componentesAgrupados || componentesAgrupados.length === 0))) {
|
||||
console.log('⚠️ Sem peças ou componentes para processar');
|
||||
setItensDisponiveis({ pecasDisponiveis: [], componentesDisponiveis: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingItens(true);
|
||||
console.log('🔄 Carregando itens disponíveis...', {
|
||||
of: formData.of_number,
|
||||
fase: formData.fase,
|
||||
processo: formData.processo_id,
|
||||
pecasCount: filteredPecas.length,
|
||||
componentesCount: componentesAgrupados?.length || 0
|
||||
});
|
||||
|
||||
try {
|
||||
const itens = await onCarregarItensDisponiveis(
|
||||
formData.of_number,
|
||||
formData.processo_id,
|
||||
filteredPecas || [],
|
||||
componentesAgrupados || []
|
||||
);
|
||||
|
||||
console.log('✅ Itens carregados:', {
|
||||
pecasDisponiveis: itens?.pecasDisponiveis?.length || 0,
|
||||
componentesDisponiveis: itens?.componentesDisponiveis?.length || 0
|
||||
});
|
||||
|
||||
setItensDisponiveis(itens || { pecasDisponiveis: [], componentesDisponiveis: [] });
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao carregar itens:', error);
|
||||
setItensDisponiveis({ pecasDisponiveis: [], componentesDisponiveis: [] });
|
||||
} finally {
|
||||
setLoadingItens(false);
|
||||
}
|
||||
}, [
|
||||
formData.of_number,
|
||||
formData.fase,
|
||||
formData.processo_id,
|
||||
filteredPecas,
|
||||
componentesAgrupados,
|
||||
onCarregarItensDisponiveis
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
carregarItensDisponiveis();
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [carregarItensDisponiveis]);
|
||||
|
||||
useEffect(() => {
|
||||
setItemSelecionado(null);
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
quantidade_produzida: '',
|
||||
todas_disponiveis: false
|
||||
}));
|
||||
}, [formData.of_number, formData.fase, formData.processo_id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (formData.todas_disponiveis && itemSelecionado) {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
quantidade_produzida: itemSelecionado.quantidade_disponivel.toString()
|
||||
}));
|
||||
}
|
||||
}, [formData.todas_disponiveis, itemSelecionado]);
|
||||
|
||||
const handleOFChange = (ofNumber: string) => {
|
||||
const updates = {
|
||||
of_number: ofNumber,
|
||||
fase: '',
|
||||
processo_id: '',
|
||||
quantidade_produzida: '',
|
||||
todas_disponiveis: false
|
||||
};
|
||||
setFormData(prev => ({ ...prev, ...updates }));
|
||||
updateCache({ of_number: ofNumber, fase: '', processo_id: '' });
|
||||
setItemSelecionado(null);
|
||||
};
|
||||
|
||||
const handleFaseChange = (fase: string) => {
|
||||
const updates = {
|
||||
fase: fase,
|
||||
processo_id: '',
|
||||
quantidade_produzida: '',
|
||||
todas_disponiveis: false
|
||||
};
|
||||
setFormData(prev => ({ ...prev, ...updates }));
|
||||
updateCache({ fase: fase, processo_id: '' });
|
||||
setItemSelecionado(null);
|
||||
};
|
||||
|
||||
const handleProcessoChange = (processoId: string) => {
|
||||
console.log('🔄 Mudança de processo detectada, limpando cache específico...');
|
||||
|
||||
// LIMPAR CACHE ESPECÍFICO ANTES DE ALTERAR O PROCESSO
|
||||
if (formData.of_number && formData.fase) {
|
||||
console.log(`🗑️ Invalidando cache para OF: ${formData.of_number}, Fase: ${formData.fase}`);
|
||||
onInvalidateCache(formData.of_number, formData.fase);
|
||||
}
|
||||
|
||||
const updates = {
|
||||
processo_id: processoId,
|
||||
quantidade_produzida: '',
|
||||
todas_disponiveis: false
|
||||
};
|
||||
setFormData(prev => ({ ...prev, ...updates }));
|
||||
updateCache({ processo_id: processoId });
|
||||
setItemSelecionado(null);
|
||||
|
||||
// Limpar itens disponíveis imediatamente para forçar nova consulta
|
||||
setItensDisponiveis({ pecasDisponiveis: [], componentesDisponiveis: [] });
|
||||
|
||||
console.log('✅ Cache limpo, nova consulta será realizada automaticamente');
|
||||
};
|
||||
|
||||
const handleItemSelect = useCallback((item: ItemDisponivel) => {
|
||||
console.log('🎯 Item selecionado:', {
|
||||
marca: item.marca,
|
||||
tipo: item.tipo,
|
||||
quantidade_disponivel: item.quantidade_disponivel
|
||||
});
|
||||
setItemSelecionado(item);
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
quantidade_produzida: '',
|
||||
todas_disponiveis: false
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handleQuantidadeChange = (value: string) => {
|
||||
const quantidade = parseInt(value);
|
||||
|
||||
if (itemSelecionado && quantidade > itemSelecionado.quantidade_disponivel) {
|
||||
toast.error(`Quantidade não pode ser maior que ${itemSelecionado.quantidade_disponivel} unidades disponíveis`);
|
||||
return;
|
||||
}
|
||||
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
quantidade_produzida: value,
|
||||
todas_disponiveis: false
|
||||
}));
|
||||
};
|
||||
|
||||
const handleTodasDisponiveisChange = (checked: boolean) => {
|
||||
console.log('🔄 Checkbox "Todas" alterado:', {
|
||||
checked,
|
||||
itemSelecionado: itemSelecionado?.marca,
|
||||
quantidade_disponivel: itemSelecionado?.quantidade_disponivel
|
||||
});
|
||||
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
todas_disponiveis: checked,
|
||||
quantidade_produzida: checked && itemSelecionado ? itemSelecionado.quantidade_disponivel.toString() : prev.quantidade_produzida
|
||||
}));
|
||||
};
|
||||
|
||||
const resetFormForNewEntry = async () => {
|
||||
console.log('🔄 Resetando formulário para nova entrada...');
|
||||
setItemSelecionado(null);
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
quantidade_produzida: '',
|
||||
observacoes: '',
|
||||
todas_disponiveis: false
|
||||
}));
|
||||
|
||||
console.log('📋 Recarregando itens disponíveis após reset...');
|
||||
await carregarItensDisponiveis();
|
||||
};
|
||||
|
||||
const handleBatchSelect = async (items: ItemDisponivel[], tipo: 'peca' | 'componente') => {
|
||||
setSaving(true);
|
||||
let sucessos = 0;
|
||||
let erros = 0;
|
||||
|
||||
try {
|
||||
for (const item of items) {
|
||||
try {
|
||||
const validacao = await onValidarSequencia(
|
||||
formData.of_number,
|
||||
item.marca,
|
||||
formData.processo_id,
|
||||
item.quantidade_disponivel,
|
||||
formData.fase
|
||||
);
|
||||
|
||||
if (!validacao.valido) {
|
||||
erros++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const apontamentoData: any = {
|
||||
of_number: formData.of_number,
|
||||
tipo_apontamento: item.tipo,
|
||||
processo_id: formData.processo_id,
|
||||
quantidade_produzida: item.quantidade_disponivel,
|
||||
data_apontamento: formData.data_apontamento,
|
||||
observacoes: `Apontamento em lote - ${tipo}`
|
||||
};
|
||||
|
||||
if (item.tipo === 'componente') {
|
||||
apontamentoData.componente_id = item.id;
|
||||
} else {
|
||||
apontamentoData.peca_id = item.id;
|
||||
}
|
||||
|
||||
const result = await onCriarApontamento(apontamentoData);
|
||||
|
||||
if (result.success) {
|
||||
sucessos++;
|
||||
} else {
|
||||
erros++;
|
||||
}
|
||||
} catch (error) {
|
||||
erros++;
|
||||
}
|
||||
}
|
||||
|
||||
if (sucessos > 0) {
|
||||
toast.success(`${sucessos} apontamentos realizados com sucesso!`);
|
||||
await Promise.all([
|
||||
onRefetch(),
|
||||
resetFormForNewEntry()
|
||||
]);
|
||||
}
|
||||
|
||||
if (erros > 0) {
|
||||
toast.error(`${erros} apontamentos falharam.`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
toast.error('Erro ao realizar apontamento em lote');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!itemSelecionado || !formData.processo_id || !formData.quantidade_produzida) {
|
||||
toast.error('Preencha todos os campos obrigatórios');
|
||||
return;
|
||||
}
|
||||
|
||||
const quantidade = parseInt(formData.quantidade_produzida);
|
||||
|
||||
if (quantidade <= 0 || quantidade > itemSelecionado.quantidade_disponivel) {
|
||||
toast.error('Quantidade inválida');
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
|
||||
try {
|
||||
console.log('🔍 Iniciando validação de sequência:', {
|
||||
of: formData.of_number,
|
||||
marca: itemSelecionado.marca,
|
||||
processo: formData.processo_id,
|
||||
quantidade,
|
||||
metodo: formData.todas_disponiveis ? 'CHECKBOX_TODAS' : 'MANUAL'
|
||||
});
|
||||
|
||||
const validacao = await onValidarSequencia(
|
||||
formData.of_number,
|
||||
itemSelecionado.marca,
|
||||
formData.processo_id,
|
||||
quantidade,
|
||||
formData.fase
|
||||
);
|
||||
|
||||
if (!validacao.valido) {
|
||||
toast.error(validacao.erro);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('✅ Validação aprovada, criando apontamento...');
|
||||
|
||||
const apontamentoData: any = {
|
||||
of_number: formData.of_number,
|
||||
tipo_apontamento: itemSelecionado.tipo,
|
||||
processo_id: formData.processo_id,
|
||||
quantidade_produzida: quantidade,
|
||||
data_apontamento: formData.data_apontamento,
|
||||
observacoes: formData.observacoes || null
|
||||
};
|
||||
|
||||
if (itemSelecionado.tipo === 'componente') {
|
||||
apontamentoData.componente_id = itemSelecionado.id;
|
||||
} else {
|
||||
apontamentoData.peca_id = itemSelecionado.id;
|
||||
}
|
||||
|
||||
console.log('📝 Dados do apontamento sendo criado:', {
|
||||
...apontamentoData,
|
||||
metodo_utilizado: formData.todas_disponiveis ? 'CHECKBOX_TODAS' : 'DIGITACAO_MANUAL',
|
||||
quantidade_original_disponivel: itemSelecionado.quantidade_disponivel,
|
||||
quantidade_sendo_apontada: quantidade
|
||||
});
|
||||
|
||||
const result = await onCriarApontamento(apontamentoData);
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ Apontamento criado com sucesso!');
|
||||
toast.success('Apontamento registrado com sucesso!');
|
||||
|
||||
// Atualizar estado do último apontamento
|
||||
if (result.success) {
|
||||
// Para desfazer, buscar o último apontamento criado
|
||||
const { data: lastApontamento } = await supabase
|
||||
.from('apontamentos_producao')
|
||||
.select('id')
|
||||
.eq('created_by', user.id)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (lastApontamento) {
|
||||
setLastApontamentoId(lastApontamento.id);
|
||||
setCanUndo(true);
|
||||
}
|
||||
}
|
||||
|
||||
// INVALIDAR CACHE ESPECÍFICO DA OF E FASE ANTES DE RECARREGAR
|
||||
console.log('🗑️ Invalidando cache após apontamento bem-sucedido...');
|
||||
onInvalidateCache(formData.of_number, formData.fase);
|
||||
|
||||
// Aguardar atualização e reset
|
||||
await Promise.all([
|
||||
onRefetch(),
|
||||
resetFormForNewEntry()
|
||||
]);
|
||||
|
||||
console.log('✅ Formulário resetado e dados atualizados após apontamento');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao registrar apontamento:', error);
|
||||
toast.error('Erro ao registrar apontamento');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const limparCacheCompleto = () => {
|
||||
localStorage.removeItem('apontamento_form_cache');
|
||||
setFormData({
|
||||
of_number: '',
|
||||
fase: '',
|
||||
data_apontamento: new Date().toISOString().split('T')[0],
|
||||
processo_id: '',
|
||||
quantidade_produzida: '',
|
||||
observacoes: '',
|
||||
todas_disponiveis: false
|
||||
});
|
||||
setItemSelecionado(null);
|
||||
setItensDisponiveis({ pecasDisponiveis: [], componentesDisponiveis: [] });
|
||||
toast.success('Cache limpo completamente!');
|
||||
};
|
||||
|
||||
// Função para desfazer último apontamento
|
||||
const handleUndo = useCallback(async () => {
|
||||
if (!lastApontamentoId || !user?.id) return;
|
||||
|
||||
setUndoLoading(true);
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('apontamentos_producao')
|
||||
.delete()
|
||||
.eq('id', lastApontamentoId)
|
||||
.eq('created_by', user.id);
|
||||
|
||||
if (error) {
|
||||
console.error('Erro ao desfazer apontamento:', error);
|
||||
toast.error('Erro ao desfazer apontamento');
|
||||
return;
|
||||
}
|
||||
|
||||
// Resetar estado do botão de desfazer
|
||||
setLastApontamentoId(null);
|
||||
setCanUndo(false);
|
||||
|
||||
// Recarregar itens disponíveis se necessário
|
||||
if (formData.of_number && formData.fase && formData.processo_id) {
|
||||
await carregarItensDisponiveis();
|
||||
}
|
||||
|
||||
toast.success('Apontamento desfeito com sucesso!');
|
||||
} catch (error) {
|
||||
console.error('Erro inesperado ao desfazer:', error);
|
||||
toast.error('Erro inesperado ao desfazer apontamento');
|
||||
} finally {
|
||||
setUndoLoading(false);
|
||||
}
|
||||
}, [lastApontamentoId, user?.id, supabase, formData.of_number, formData.fase, formData.processo_id, carregarItensDisponiveis]);
|
||||
|
||||
const processoSelecionado = processosOrdenados?.find(p => p.id === formData.processo_id);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-8 space-y-4">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
<span className="text-muted-foreground">Carregando dados...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
{/* Cache status */}
|
||||
{(formData.of_number || formData.fase || formData.processo_id) && (
|
||||
<Alert>
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertDescription className="flex items-center justify-between">
|
||||
<span>Formulário restaurado do cache</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={limparCacheCompleto}
|
||||
className="ml-2"
|
||||
>
|
||||
<RefreshCw className="h-3 w-3 mr-1" />
|
||||
Limpar Cache
|
||||
</Button>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Campos de seleção básicos */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="of">Ordem de Fabricação *</Label>
|
||||
<Select value={formData.of_number} onValueChange={handleOFChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecione a OF" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(ofs || []).map((of) => (
|
||||
<SelectItem key={of.id} value={of.num_of}>
|
||||
{of.num_of} - {of.descritivo}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{formData.of_number && (
|
||||
<div>
|
||||
<Label htmlFor="fase">Fase *</Label>
|
||||
<Select value={formData.fase} onValueChange={handleFaseChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecione a fase" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{fasesDisponiveis.map((fase) => (
|
||||
<SelectItem key={fase} value={fase}>
|
||||
{fase}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{formData.fase && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="data">Data do Apontamento *</Label>
|
||||
<Input
|
||||
id="data"
|
||||
type="date"
|
||||
value={formData.data_apontamento}
|
||||
onChange={(e) => {
|
||||
const newDate = e.target.value;
|
||||
setFormData(prev => ({ ...prev, data_apontamento: newDate }));
|
||||
updateCache({ data_apontamento: newDate });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Processo *</Label>
|
||||
<Select value={formData.processo_id} onValueChange={handleProcessoChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecione o processo" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(processosOrdenados || []).map((processo) => (
|
||||
<SelectItem key={processo.id} value={processo.id}>
|
||||
{processo.ordem}. {processo.nome}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Informação sobre o processo */}
|
||||
{processoSelecionado && (
|
||||
<Alert>
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
{processoSelecionado.ordem === 1
|
||||
? `Processo inicial: ${processoSelecionado.nome}. Todos os itens estão disponíveis.`
|
||||
: `Processo ${processoSelecionado.ordem}: ${processoSelecionado.nome}. Apenas itens que passaram pelos processos anteriores estão disponíveis.`
|
||||
}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Seletor de Itens */}
|
||||
{formData.processo_id && (
|
||||
<SeletorPecasSimples
|
||||
pecasDisponiveis={itensDisponiveis.pecasDisponiveis || []}
|
||||
componentesDisponiveis={itensDisponiveis.componentesDisponiveis || []}
|
||||
onItemSelect={handleItemSelect}
|
||||
onBatchSelect={handleBatchSelect}
|
||||
loading={loadingItens || validacaoLoading}
|
||||
onSubmit={() => handleSubmit({} as React.FormEvent)}
|
||||
submitDisabled={saving || !itemSelecionado || !formData.processo_id || !formData.quantidade_produzida || loadingItens}
|
||||
submitLoading={saving}
|
||||
canUndo={canUndo}
|
||||
onUndo={handleUndo}
|
||||
undoLoading={undoLoading}
|
||||
lastApontamentoId={lastApontamentoId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Campos de Quantidade */}
|
||||
{itemSelecionado && (
|
||||
<div className="space-y-4 p-4 bg-slate-800/50 rounded-lg border border-slate-700">
|
||||
<h4 className="font-medium text-slate-200">Quantidade a Apontar</h4>
|
||||
<div className="space-y-3">
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
max={itemSelecionado.quantidade_disponivel}
|
||||
placeholder="Digite a quantidade..."
|
||||
value={formData.quantidade_produzida}
|
||||
onChange={(e) => handleQuantidadeChange(e.target.value)}
|
||||
disabled={formData.todas_disponiveis}
|
||||
className="w-full text-lg h-12 bg-slate-800 border-slate-600"
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="todas-disponiveis"
|
||||
checked={formData.todas_disponiveis}
|
||||
onCheckedChange={handleTodasDisponiveisChange}
|
||||
/>
|
||||
<Label htmlFor="todas-disponiveis" className="text-sm cursor-pointer">
|
||||
Todas ({itemSelecionado.quantidade_disponivel})
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Label htmlFor="observacoes">Observações</Label>
|
||||
<Textarea
|
||||
id="observacoes"
|
||||
placeholder="Observações sobre o apontamento..."
|
||||
value={formData.observacoes}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, observacoes: e.target.value }))}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card de informações do item selecionado */}
|
||||
<div>
|
||||
<Card className="bg-muted/50 h-fit">
|
||||
<CardContent className="p-4">
|
||||
<h4 className="font-medium flex items-center gap-2 mb-3">
|
||||
<Package className="h-4 w-4" />
|
||||
Informações do Item Selecionado
|
||||
</h4>
|
||||
{!itemSelecionado ? (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Selecione um item para ver as informações
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 text-sm">
|
||||
<div><strong>Tipo:</strong> {itemSelecionado.tipo === 'componente' ? 'Componente' : 'Peça'}</div>
|
||||
<div><strong>Marca:</strong> {itemSelecionado.marca}</div>
|
||||
<div><strong>OF:</strong> {formData.of_number}</div>
|
||||
<div><strong>Fase:</strong> {formData.fase}</div>
|
||||
<div><strong>Processo:</strong> {processoSelecionado?.nome || 'N/A'}</div>
|
||||
<div><strong>Descrição:</strong> {itemSelecionado.descricao || 'N/A'}</div>
|
||||
<div><strong>Quantidade Disponível:</strong> {itemSelecionado.quantidade_disponivel} unidades</div>
|
||||
{formData.quantidade_produzida && (
|
||||
<div className="pt-2 border-t">
|
||||
<div><strong>Quantidade a Apontar:</strong> {formData.quantidade_produzida} unidades</div>
|
||||
<div><strong>Método:</strong> {formData.todas_disponiveis ? 'Checkbox "Todas"' : 'Digitação Manual'}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,237 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Search, Package } from 'lucide-react';
|
||||
import { useApontamentosProducao } from '@/hooks/useApontamentosProducao';
|
||||
import { format } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
|
||||
export const ApontamentosList = () => {
|
||||
const { apontamentos, loading } = useApontamentosProducao();
|
||||
const [filtroOF, setFiltroOF] = useState('');
|
||||
const [filtroProcesso, setFiltroProcesso] = useState('');
|
||||
const [filtroData, setFiltroData] = useState('');
|
||||
const [filtroPecaComponente, setFiltroPecaComponente] = useState('');
|
||||
|
||||
const apontamentosFiltrados = apontamentos.filter(apt => {
|
||||
const matchOF = !filtroOF || apt.of_number.toLowerCase().includes(filtroOF.toLowerCase());
|
||||
const matchProcesso = !filtroProcesso || apt.processo?.nome.toLowerCase().includes(filtroProcesso.toLowerCase());
|
||||
const matchData = !filtroData || apt.data_apontamento.includes(filtroData);
|
||||
|
||||
// Filtro por peça/componente - busca na marca da peça ou componente
|
||||
const matchPecaComponente = !filtroPecaComponente ||
|
||||
getMarcaExibida(apt).toLowerCase().includes(filtroPecaComponente.toLowerCase());
|
||||
|
||||
return matchOF && matchProcesso && matchData && matchPecaComponente;
|
||||
});
|
||||
|
||||
// Função para determinar a marca exibida baseada no tipo de apontamento
|
||||
const getMarcaExibida = (apontamento: any) => {
|
||||
if (apontamento.tipo_apontamento === 'componente' && apontamento.componente?.marca_componente) {
|
||||
return apontamento.componente.marca_componente;
|
||||
}
|
||||
if (apontamento.tipo_apontamento === 'peca' && apontamento.peca?.marca) {
|
||||
return apontamento.peca.marca;
|
||||
}
|
||||
return 'N/A';
|
||||
};
|
||||
|
||||
// Função para calcular peso total baseado no tipo correto (peça ou componente)
|
||||
const calcularPesoTotal = (apontamento: any) => {
|
||||
let pesoUnitario = 0;
|
||||
|
||||
if (apontamento.tipo_apontamento === 'componente' && apontamento.componente?.peso_unitario) {
|
||||
pesoUnitario = Number(apontamento.componente.peso_unitario);
|
||||
} else if (apontamento.tipo_apontamento === 'peca' && apontamento.peca?.peso_unitario) {
|
||||
pesoUnitario = Number(apontamento.peca.peso_unitario);
|
||||
}
|
||||
|
||||
const quantidade = Number(apontamento.quantidade_produzida) || 0;
|
||||
const pesoTotal = pesoUnitario * quantidade;
|
||||
|
||||
console.log(`Apontamento ${apontamento.id}: Tipo: ${apontamento.tipo_apontamento}, Peso unitário: ${pesoUnitario}, Quantidade: ${quantidade}, Peso total: ${pesoTotal}`);
|
||||
|
||||
return pesoTotal.toFixed(3);
|
||||
};
|
||||
|
||||
// Função para obter a descrição correta baseada no tipo
|
||||
const getDescricaoExibida = (apontamento: any) => {
|
||||
if (apontamento.tipo_apontamento === 'componente' && apontamento.componente?.descricao) {
|
||||
return apontamento.componente.descricao;
|
||||
}
|
||||
if (apontamento.tipo_apontamento === 'peca' && apontamento.peca?.descricao) {
|
||||
return apontamento.peca.descricao;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
// Calcular totais para debug
|
||||
const pesoTotalApontamentos = apontamentosFiltrados.reduce((total, apt) => {
|
||||
return total + Number(calcularPesoTotal(apt));
|
||||
}, 0);
|
||||
|
||||
console.log('Total de peso dos apontamentos filtrados:', pesoTotalApontamentos);
|
||||
|
||||
if (loading) {
|
||||
return <div className="text-center py-8">Carregando apontamentos...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Filtros */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Search className="h-4 w-4" />
|
||||
Filtros
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-3">
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">OF</label>
|
||||
<Input
|
||||
placeholder="Filtrar por OF..."
|
||||
value={filtroOF}
|
||||
onChange={(e) => setFiltroOF(e.target.value)}
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">Processo</label>
|
||||
<Input
|
||||
placeholder="Filtrar por processo..."
|
||||
value={filtroProcesso}
|
||||
onChange={(e) => setFiltroProcesso(e.target.value)}
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">Data</label>
|
||||
<Input
|
||||
type="date"
|
||||
value={filtroData}
|
||||
onChange={(e) => setFiltroData(e.target.value)}
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">Peça/Componente</label>
|
||||
<Input
|
||||
placeholder="Filtrar por peça/componente..."
|
||||
value={filtroPecaComponente}
|
||||
onChange={(e) => setFiltroPecaComponente(e.target.value)}
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-start">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{apontamentosFiltrados.length} registros encontrados
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
Peso total: {pesoTotalApontamentos.toFixed(3)} kg
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Tabela de Apontamentos */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Package className="h-4 w-4" />
|
||||
Histórico de Apontamentos
|
||||
</div>
|
||||
<Badge variant="secondary">
|
||||
{apontamentosFiltrados.length} registros
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{apontamentosFiltrados.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
Nenhum apontamento encontrado
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>OF</TableHead>
|
||||
<TableHead>Peça/Componente</TableHead>
|
||||
<TableHead>Tipo</TableHead>
|
||||
<TableHead>Processo</TableHead>
|
||||
<TableHead className="text-center">Quantidade</TableHead>
|
||||
<TableHead className="text-center">Peso Unit. (kg)</TableHead>
|
||||
<TableHead className="text-center">Peso Total (kg)</TableHead>
|
||||
<TableHead>Data</TableHead>
|
||||
<TableHead>Observações</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{apontamentosFiltrados.map((apontamento) => {
|
||||
const pesoUnitario = apontamento.tipo_apontamento === 'componente'
|
||||
? Number(apontamento.componente?.peso_unitario || 0)
|
||||
: Number(apontamento.peca?.peso_unitario || 0);
|
||||
|
||||
return (
|
||||
<TableRow key={apontamento.id} className="h-9">
|
||||
<TableCell className="font-medium py-1">
|
||||
{apontamento.of_number}
|
||||
</TableCell>
|
||||
<TableCell className="py-1">
|
||||
<div className="font-medium">
|
||||
{getMarcaExibida(apontamento)}
|
||||
</div>
|
||||
{getDescricaoExibida(apontamento) && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{getDescricaoExibida(apontamento)}
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="py-1">
|
||||
<Badge variant={apontamento.tipo_apontamento === 'componente' ? 'secondary' : 'default'}>
|
||||
{apontamento.tipo_apontamento === 'componente' ? 'Componente' : 'Peça'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="py-1">
|
||||
<Badge variant="outline">
|
||||
{apontamento.processo?.nome || 'N/A'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center font-medium py-1">
|
||||
{apontamento.quantidade_produzida}
|
||||
</TableCell>
|
||||
<TableCell className="text-center py-1">
|
||||
{pesoUnitario.toFixed(3)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center font-medium py-1">
|
||||
{calcularPesoTotal(apontamento)}
|
||||
</TableCell>
|
||||
<TableCell className="py-1">
|
||||
{format(new Date(apontamento.data_apontamento), 'dd/MM/yyyy', {
|
||||
locale: ptBR
|
||||
})}
|
||||
</TableCell>
|
||||
<TableCell className="max-w-xs py-1">
|
||||
<div className="text-sm text-muted-foreground truncate">
|
||||
{apontamento.observacoes || '-'}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Package } from 'lucide-react';
|
||||
import { useApontamentosProducao } from '@/hooks/useApontamentosProducao';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { toast } from 'sonner';
|
||||
import { ApontamentosFilters } from './historico/ApontamentosFilters';
|
||||
import { ApontamentosResultsList } from './historico/ApontamentosResultsList';
|
||||
import { useApontamentosFilters } from './historico/useApontamentosFilters';
|
||||
|
||||
export const ApontamentosListOtimizado: React.FC = () => {
|
||||
const { apontamentos, loading, refetch } = useApontamentosProducao();
|
||||
|
||||
const {
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
filterOF,
|
||||
filterFase,
|
||||
filterProcesso,
|
||||
dataInicio,
|
||||
dataFim,
|
||||
setDataInicio,
|
||||
setDataFim,
|
||||
uniqueOFs,
|
||||
uniqueFases,
|
||||
uniqueProcessos,
|
||||
filteredApontamentos,
|
||||
initialized,
|
||||
handleOFChange,
|
||||
handleFaseChange,
|
||||
handleProcessoChange,
|
||||
clearFilters
|
||||
} = useApontamentosFilters(apontamentos);
|
||||
|
||||
const handleReverterApontamento = async (apontamentoId: string) => {
|
||||
try {
|
||||
console.log('Iniciando reversão do apontamento:', apontamentoId);
|
||||
|
||||
// Buscar informações do apontamento antes de deletar para logs
|
||||
const { data: apontamentoInfo } = await supabase
|
||||
.from('apontamentos_producao')
|
||||
.select(`
|
||||
of_number,
|
||||
quantidade_produzida,
|
||||
peca:pecas(marca),
|
||||
componente:componentes_peca(marca_componente),
|
||||
processo:processos_fabricacao(nome)
|
||||
`)
|
||||
.eq('id', apontamentoId)
|
||||
.single();
|
||||
|
||||
if (apontamentoInfo) {
|
||||
const marca = apontamentoInfo.peca?.marca || apontamentoInfo.componente?.marca_componente || 'N/A';
|
||||
console.log(`Revertendo apontamento: ${marca} - ${apontamentoInfo.quantidade_produzida} unidades - OF: ${apontamentoInfo.of_number}`);
|
||||
}
|
||||
|
||||
// Deletar o apontamento
|
||||
const { error } = await supabase
|
||||
.from('apontamentos_producao')
|
||||
.delete()
|
||||
.eq('id', apontamentoId);
|
||||
|
||||
if (error) {
|
||||
console.error('Erro ao deletar apontamento:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.log('Apontamento deletado com sucesso, atualizando lista...');
|
||||
toast.success('Apontamento revertido com sucesso!');
|
||||
|
||||
// Forçar atualização dos dados
|
||||
await refetch();
|
||||
console.log('Lista de apontamentos atualizada');
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Erro completo ao reverter apontamento:', error);
|
||||
toast.error(`Erro ao reverter apontamento: ${error.message || 'Erro desconhecido'}`);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading || !initialized) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className="flex items-center gap-2">
|
||||
<Package className="h-5 w-5 animate-spin" />
|
||||
<span>Carregando histórico...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<ApontamentosFilters
|
||||
searchTerm={searchTerm}
|
||||
setSearchTerm={setSearchTerm}
|
||||
filterOF={filterOF}
|
||||
filterFase={filterFase}
|
||||
filterProcesso={filterProcesso}
|
||||
dataInicio={dataInicio}
|
||||
dataFim={dataFim}
|
||||
setDataInicio={setDataInicio}
|
||||
setDataFim={setDataFim}
|
||||
uniqueOFs={uniqueOFs}
|
||||
uniqueFases={uniqueFases}
|
||||
uniqueProcessos={uniqueProcessos}
|
||||
handleOFChange={handleOFChange}
|
||||
handleFaseChange={handleFaseChange}
|
||||
handleProcessoChange={handleProcessoChange}
|
||||
clearFilters={clearFilters}
|
||||
/>
|
||||
|
||||
<ApontamentosResultsList
|
||||
filteredApontamentos={filteredApontamentos}
|
||||
onRevert={handleReverterApontamento}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,258 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Plus, Edit, Save, X, ChevronUp, ChevronDown } from 'lucide-react';
|
||||
import { useApontamentosProducao } from '@/hooks/useApontamentosProducao';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
type SortField = 'ordem' | 'nome' | 'descricao' | 'ativo';
|
||||
type SortDirection = 'asc' | 'desc';
|
||||
|
||||
export const ProcessosList = () => {
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [sortField, setSortField] = useState<SortField>('ordem');
|
||||
const [sortDirection, setSortDirection] = useState<SortDirection>('asc');
|
||||
const [formData, setFormData] = useState({
|
||||
nome: '',
|
||||
descricao: '',
|
||||
ordem: 0
|
||||
});
|
||||
|
||||
const { processos, loading, criarProcesso, atualizarProcesso } = useApontamentosProducao();
|
||||
|
||||
const handleSort = (field: SortField) => {
|
||||
if (sortField === field) {
|
||||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortDirection('asc');
|
||||
}
|
||||
};
|
||||
|
||||
const sortedProcessos = React.useMemo(() => {
|
||||
return [...processos].sort((a, b) => {
|
||||
let aValue: any;
|
||||
let bValue: any;
|
||||
|
||||
switch (sortField) {
|
||||
case 'ordem':
|
||||
aValue = a.ordem;
|
||||
bValue = b.ordem;
|
||||
break;
|
||||
case 'nome':
|
||||
aValue = a.nome;
|
||||
bValue = b.nome;
|
||||
break;
|
||||
case 'descricao':
|
||||
aValue = a.descricao || '';
|
||||
bValue = b.descricao || '';
|
||||
break;
|
||||
case 'ativo':
|
||||
aValue = a.ativo;
|
||||
bValue = b.ativo;
|
||||
break;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (aValue < bValue) return sortDirection === 'asc' ? -1 : 1;
|
||||
if (aValue > bValue) return sortDirection === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
}, [processos, sortField, sortDirection]);
|
||||
|
||||
const SortButton = ({ field, children }: { field: SortField; children: React.ReactNode }) => (
|
||||
<button
|
||||
onClick={() => handleSort(field)}
|
||||
className="flex items-center gap-1 hover:text-foreground transition-colors w-full text-left"
|
||||
>
|
||||
{children}
|
||||
{sortField === field && (
|
||||
sortDirection === 'asc' ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!formData.nome.trim()) {
|
||||
toast.error('Nome do processo é obrigatório');
|
||||
return;
|
||||
}
|
||||
|
||||
let result;
|
||||
if (editingId) {
|
||||
result = await atualizarProcesso(editingId, formData);
|
||||
} else {
|
||||
result = await criarProcesso({
|
||||
...formData,
|
||||
ativo: true
|
||||
});
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
setFormData({ nome: '', descricao: '', ordem: 0 });
|
||||
setShowForm(false);
|
||||
setEditingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (processo: any) => {
|
||||
setFormData({
|
||||
nome: processo.nome,
|
||||
descricao: processo.descricao || '',
|
||||
ordem: processo.ordem || 0
|
||||
});
|
||||
setEditingId(processo.id);
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setFormData({ nome: '', descricao: '', ordem: 0 });
|
||||
setShowForm(false);
|
||||
setEditingId(null);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-32">
|
||||
<div className="text-muted-foreground">Carregando processos...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-lg font-semibold">Processos de Fabricação</h3>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => setShowForm(!showForm)}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Novo Processo
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{showForm && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">
|
||||
{editingId ? 'Editar Processo' : 'Novo Processo'}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="nome">Nome *</Label>
|
||||
<Input
|
||||
id="nome"
|
||||
value={formData.nome}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, nome: e.target.value }))}
|
||||
placeholder="Nome do processo"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="ordem">Ordem</Label>
|
||||
<Input
|
||||
id="ordem"
|
||||
type="number"
|
||||
min="0"
|
||||
value={formData.ordem}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, ordem: parseInt(e.target.value) || 0 }))}
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="descricao">Descrição</Label>
|
||||
<Textarea
|
||||
id="descricao"
|
||||
value={formData.descricao}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, descricao: e.target.value }))}
|
||||
placeholder="Descrição opcional"
|
||||
rows={1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="outline" onClick={handleCancel}>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit">
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
{editingId ? 'Atualizar' : 'Salvar'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div className="border rounded-lg">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="h-6">
|
||||
<TableHead className="py-1">
|
||||
<SortButton field="ordem">Ordem</SortButton>
|
||||
</TableHead>
|
||||
<TableHead className="py-1">
|
||||
<SortButton field="nome">Nome</SortButton>
|
||||
</TableHead>
|
||||
<TableHead className="py-1">
|
||||
<SortButton field="descricao">Descrição</SortButton>
|
||||
</TableHead>
|
||||
<TableHead className="py-1">
|
||||
<SortButton field="ativo">Status</SortButton>
|
||||
</TableHead>
|
||||
<TableHead className="py-1 w-24">Ações</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{sortedProcessos.map((processo) => (
|
||||
<TableRow key={processo.id} className="h-6">
|
||||
<TableCell className="py-1 text-center text-sm">{processo.ordem}</TableCell>
|
||||
<TableCell className="py-1 font-medium text-sm">{processo.nome}</TableCell>
|
||||
<TableCell className="py-1 text-sm">{processo.descricao || '-'}</TableCell>
|
||||
<TableCell className="py-1">
|
||||
<Badge variant={processo.ativo ? 'default' : 'secondary'} className="text-xs">
|
||||
{processo.ativo ? 'Ativo' : 'Inativo'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="py-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(processo)}
|
||||
className="h-5 w-5 p-0"
|
||||
>
|
||||
<Edit className="h-3 w-3" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<Card className="mt-4">
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<p><strong>Informações:</strong></p>
|
||||
<p>Os processos são utilizados para categorizar os apontamentos de produção.</p>
|
||||
<p>A ordem define a sequência dos processos no fluxo de fabricação.</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,412 @@
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Package, Search, CheckSquare, Loader2, Weight, Info, ArrowUpDown, Undo2 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { applyMarcaFilter } from '@/utils/rangeFilter';
|
||||
import { naturalSort } from '@/utils/naturalSort';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
|
||||
interface ItemDisponivel {
|
||||
id: string;
|
||||
marca: string;
|
||||
descricao: string;
|
||||
tipo: 'peca' | 'componente';
|
||||
quantidade_disponivel: number;
|
||||
processo_atual_permitido: number;
|
||||
peso_unitario?: number;
|
||||
}
|
||||
|
||||
interface SeletorPecasSimplesProps {
|
||||
pecasDisponiveis: ItemDisponivel[];
|
||||
componentesDisponiveis: ItemDisponivel[];
|
||||
onItemSelect: (item: ItemDisponivel) => void;
|
||||
onBatchSelect: (items: ItemDisponivel[], tipo: 'peca' | 'componente') => void;
|
||||
loading: boolean;
|
||||
onSubmit?: () => void;
|
||||
submitDisabled?: boolean;
|
||||
submitLoading?: boolean;
|
||||
// Props para o botão de desfazer
|
||||
canUndo?: boolean;
|
||||
onUndo?: () => void;
|
||||
undoLoading?: boolean;
|
||||
lastApontamentoId?: string;
|
||||
}
|
||||
|
||||
export const SeletorPecasSimples: React.FC<SeletorPecasSimplesProps> = ({
|
||||
pecasDisponiveis = [],
|
||||
componentesDisponiveis = [],
|
||||
onItemSelect,
|
||||
onBatchSelect,
|
||||
loading,
|
||||
onSubmit,
|
||||
submitDisabled = false,
|
||||
submitLoading = false,
|
||||
canUndo = false,
|
||||
onUndo,
|
||||
undoLoading = false,
|
||||
lastApontamentoId
|
||||
}) => {
|
||||
const [filtro, setFiltro] = useState('');
|
||||
const [activeTab, setActiveTab] = useState('pecas');
|
||||
const [itemSelecionado, setItemSelecionado] = useState<ItemDisponivel | null>(null);
|
||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
|
||||
|
||||
// Filtrar e ordenar peças com suporte a range
|
||||
const pecasFiltradas = useMemo(() => {
|
||||
let pecas = pecasDisponiveis;
|
||||
if (filtro) {
|
||||
pecas = pecasDisponiveis.filter(peca =>
|
||||
applyMarcaFilter(peca.marca, filtro) ||
|
||||
peca.descricao.toLowerCase().includes(filtro.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
// Ordenar numericamente usando naturalSort
|
||||
return [...pecas].sort((a, b) => {
|
||||
const comparison = naturalSort(a.marca, b.marca);
|
||||
return sortOrder === 'asc' ? comparison : -comparison;
|
||||
});
|
||||
}, [pecasDisponiveis, filtro, sortOrder]);
|
||||
|
||||
// Filtrar e ordenar componentes com suporte a range
|
||||
const componentesFiltrados = useMemo(() => {
|
||||
let componentes = componentesDisponiveis;
|
||||
if (filtro) {
|
||||
componentes = componentesDisponiveis.filter(comp =>
|
||||
applyMarcaFilter(comp.marca, filtro) ||
|
||||
comp.descricao.toLowerCase().includes(filtro.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
// Ordenar numericamente usando naturalSort
|
||||
return [...componentes].sort((a, b) => {
|
||||
const comparison = naturalSort(a.marca, b.marca);
|
||||
return sortOrder === 'asc' ? comparison : -comparison;
|
||||
});
|
||||
}, [componentesDisponiveis, filtro, sortOrder]);
|
||||
|
||||
// Calcular peso total das peças disponíveis/filtradas
|
||||
const pesoTotalPecas = useMemo(() => {
|
||||
return pecasFiltradas.reduce((total, peca) => {
|
||||
const pesoUnitario = peca.peso_unitario || 0;
|
||||
return total + (pesoUnitario * peca.quantidade_disponivel);
|
||||
}, 0);
|
||||
}, [pecasFiltradas]);
|
||||
|
||||
// Calcular peso total dos componentes disponíveis/filtrados
|
||||
const pesoTotalComponentes = useMemo(() => {
|
||||
return componentesFiltrados.reduce((total, comp) => {
|
||||
const pesoUnitario = comp.peso_unitario || 0;
|
||||
return total + (pesoUnitario * comp.quantidade_disponivel);
|
||||
}, 0);
|
||||
}, [componentesFiltrados]);
|
||||
|
||||
const handleItemClick = (item: ItemDisponivel) => {
|
||||
setItemSelecionado(item);
|
||||
onItemSelect(item);
|
||||
};
|
||||
|
||||
const handleBatchPecas = () => {
|
||||
if (pecasFiltradas.length === 0) {
|
||||
toast.error('Nenhuma peça disponível para apontamento em lote');
|
||||
return;
|
||||
}
|
||||
onBatchSelect(pecasFiltradas, 'peca');
|
||||
};
|
||||
|
||||
const handleBatchComponentes = () => {
|
||||
if (componentesFiltrados.length === 0) {
|
||||
toast.error('Nenhum componente disponível para apontamento em lote');
|
||||
return;
|
||||
}
|
||||
onBatchSelect(componentesFiltrados, 'componente');
|
||||
};
|
||||
|
||||
const formatarPeso = (peso: number) => {
|
||||
return peso.toFixed(2);
|
||||
};
|
||||
|
||||
const handleToggleSort = () => {
|
||||
setSortOrder(current => current === 'asc' ? 'desc' : 'asc');
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card className="bg-card border-border">
|
||||
<CardContent className="flex items-center justify-center p-8">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">Carregando itens disponíveis...</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-card-foreground flex items-center gap-2 text-lg">
|
||||
<Package className="h-5 w-5" />
|
||||
Itens Disponíveis
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleToggleSort}
|
||||
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<ArrowUpDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Ordenar {sortOrder === 'asc' ? 'crescente' : 'decrescente'}</p>
|
||||
<p className="text-xs text-muted-foreground">Clique para alternar</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
{/* Botão Desfazer Último Apontamento */}
|
||||
{onUndo && (
|
||||
<AlertDialog>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!canUndo || undoLoading}
|
||||
className="border-orange-200 text-orange-600 hover:bg-orange-50 hover:text-orange-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{undoLoading ? (
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Undo2 className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
{undoLoading ? 'Desfazendo...' : 'Desfazer Ult. Apont.'}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Desfazer o último apontamento realizado</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{canUndo ? 'Clique para desfazer' : 'Nenhum apontamento para desfazer'}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Confirmar Reversão</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Tem certeza que deseja desfazer o último apontamento?
|
||||
<br />
|
||||
<strong>Esta ação não pode ser desfeita.</strong>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={onUndo}
|
||||
className="bg-orange-600 hover:bg-orange-700"
|
||||
>
|
||||
Confirmar Reversão
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
|
||||
{onSubmit && (
|
||||
<Button
|
||||
onClick={onSubmit}
|
||||
disabled={submitDisabled || submitLoading}
|
||||
className="min-w-32"
|
||||
>
|
||||
{submitLoading ? 'Salvando...' : 'Registrar Apontamento'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Campo de busca com tooltip */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Filtrar por número da peça... (ex: 32@47)"
|
||||
value={filtro}
|
||||
onChange={(e) => setFiltro(e.target.value)}
|
||||
className="pl-10 pr-8 bg-background border-border"
|
||||
/>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="absolute right-3 top-3 h-4 w-4 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="text-sm space-y-1">
|
||||
<p><strong>Filtro por range:</strong></p>
|
||||
<p>Use @ para filtrar intervalos</p>
|
||||
<p><strong>Exemplo:</strong> 32@47 (peças de 32 a 47)</p>
|
||||
<p><strong>Busca normal:</strong> Digite parte da marca</p>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="w-full bg-muted border-border">
|
||||
<TabsTrigger
|
||||
value="pecas"
|
||||
className="flex-1 text-muted-foreground data-[state=active]:bg-background data-[state=active]:text-foreground"
|
||||
>
|
||||
<Package className="h-4 w-4 mr-2" />
|
||||
Peças ({pecasFiltradas.length}) - {formatarPeso(pesoTotalPecas)} kg
|
||||
<Weight className="h-3 w-3 ml-1 text-muted-foreground" />
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="componentes"
|
||||
className="flex-1 text-muted-foreground data-[state=active]:bg-background data-[state=active]:text-foreground"
|
||||
>
|
||||
<Package className="h-4 w-4 mr-2" />
|
||||
Componentes ({componentesFiltrados.length}) - {formatarPeso(pesoTotalComponentes)} kg
|
||||
<Weight className="h-3 w-3 ml-1 text-muted-foreground" />
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="pecas" className="space-y-3 mt-4">
|
||||
{pecasFiltradas.length > 0 && (
|
||||
<Button
|
||||
onClick={handleBatchPecas}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full border-border hover:bg-muted"
|
||||
>
|
||||
<CheckSquare className="h-4 w-4 mr-2" />
|
||||
Apontar Todas as Peças ({pecasFiltradas.length})
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div className="space-y-2 max-h-64 overflow-y-auto">
|
||||
{pecasFiltradas.length === 0 ? (
|
||||
<div className="text-center py-6 text-muted-foreground">
|
||||
<Package className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
||||
<p>Nenhuma peça disponível</p>
|
||||
</div>
|
||||
) : (
|
||||
pecasFiltradas.map((peca) => (
|
||||
<div
|
||||
key={peca.id}
|
||||
onClick={() => handleItemClick(peca)}
|
||||
className={`p-3 rounded-lg border cursor-pointer transition-all duration-200 ${
|
||||
itemSelecionado?.id === peca.id
|
||||
? 'bg-primary/10 border-primary text-foreground'
|
||||
: 'bg-background border-border hover:bg-muted text-foreground'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Package className="h-4 w-4 text-primary" />
|
||||
<div>
|
||||
<div className="font-medium">{peca.marca}</div>
|
||||
<div className="text-sm text-muted-foreground">{peca.descricao}</div>
|
||||
{peca.peso_unitario && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Peso unitário: {formatarPeso(peca.peso_unitario)} kg
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="secondary" className="bg-secondary text-secondary-foreground">
|
||||
{peca.quantidade_disponivel} un.
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="componentes" className="space-y-3 mt-4">
|
||||
{componentesFiltrados.length > 0 && (
|
||||
<Button
|
||||
onClick={handleBatchComponentes}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full border-border hover:bg-muted"
|
||||
>
|
||||
<CheckSquare className="h-4 w-4 mr-2" />
|
||||
Apontar Todos os Componentes ({componentesFiltrados.length})
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div className="space-y-2 max-h-64 overflow-y-auto">
|
||||
{componentesFiltrados.length === 0 ? (
|
||||
<div className="text-center py-6 text-muted-foreground">
|
||||
<Package className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
||||
<p>Nenhum componente disponível</p>
|
||||
</div>
|
||||
) : (
|
||||
componentesFiltrados.map((comp) => (
|
||||
<div
|
||||
key={comp.id}
|
||||
onClick={() => handleItemClick(comp)}
|
||||
className={`p-3 rounded-lg border cursor-pointer transition-all duration-200 ${
|
||||
itemSelecionado?.id === comp.id
|
||||
? 'bg-primary/10 border-primary text-foreground'
|
||||
: 'bg-background border-border hover:bg-muted text-foreground'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Package className="h-4 w-4 text-orange-500" />
|
||||
<div>
|
||||
<div className="font-medium">{comp.marca}</div>
|
||||
<div className="text-sm text-muted-foreground">{comp.descricao}</div>
|
||||
{comp.peso_unitario && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Peso unitário: {formatarPeso(comp.peso_unitario)} kg
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="secondary" className="bg-secondary text-secondary-foreground">
|
||||
{comp.quantidade_disponivel} un.
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,195 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Calendar } from '@/components/ui/calendar';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Search, Calendar as CalendarIcon, Filter, RefreshCw } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface ApontamentosFiltersProps {
|
||||
searchTerm: string;
|
||||
setSearchTerm: (value: string) => void;
|
||||
filterOF: string;
|
||||
filterFase: string;
|
||||
filterProcesso: string;
|
||||
dataInicio: Date | undefined;
|
||||
dataFim: Date | undefined;
|
||||
setDataInicio: (date: Date | undefined) => void;
|
||||
setDataFim: (date: Date | undefined) => void;
|
||||
uniqueOFs: string[];
|
||||
uniqueFases: string[];
|
||||
uniqueProcessos: string[];
|
||||
handleOFChange: (value: string) => void;
|
||||
handleFaseChange: (value: string) => void;
|
||||
handleProcessoChange: (value: string) => void;
|
||||
clearFilters: () => void;
|
||||
}
|
||||
|
||||
export const ApontamentosFilters: React.FC<ApontamentosFiltersProps> = ({
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
filterOF,
|
||||
filterFase,
|
||||
filterProcesso,
|
||||
dataInicio,
|
||||
dataFim,
|
||||
setDataInicio,
|
||||
setDataFim,
|
||||
uniqueOFs,
|
||||
uniqueFases,
|
||||
uniqueProcessos,
|
||||
handleOFChange,
|
||||
handleFaseChange,
|
||||
handleProcessoChange,
|
||||
clearFilters
|
||||
}) => {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm flex items-center gap-2">
|
||||
<Filter className="h-4 w-4" />
|
||||
Filtros Inteligentes
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Primeira linha de filtros - mais compacta */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-2">
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-medium">Buscar</label>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 h-3 w-3 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="OF, Marca..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-7 h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-medium">OF</label>
|
||||
<Select value={filterOF || 'all'} onValueChange={handleOFChange}>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="Selecione" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Todas as OFs</SelectItem>
|
||||
{uniqueOFs.map((of) => (
|
||||
<SelectItem key={of} value={of}>{of}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-medium">Fase</label>
|
||||
<Select value={filterFase || 'all'} onValueChange={handleFaseChange}>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="Selecione" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Todas as fases</SelectItem>
|
||||
{uniqueFases.map((fase) => (
|
||||
<SelectItem key={fase} value={fase}>{fase}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-medium">Processo</label>
|
||||
<Select value={filterProcesso || 'all'} onValueChange={handleProcessoChange}>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="Selecione" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Todos os processos</SelectItem>
|
||||
{uniqueProcessos.map((processo) => (
|
||||
<SelectItem key={processo} value={processo}>{processo}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-end">
|
||||
<Button variant="outline" onClick={clearFilters} className="h-8 text-xs">
|
||||
<RefreshCw className="h-3 w-3 mr-1" />
|
||||
Limpar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Segunda linha - filtros de data */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-2">
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-medium">Data Início</label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"h-8 justify-start text-left font-normal text-xs",
|
||||
!dataInicio && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<CalendarIcon className="mr-1 h-3 w-3" />
|
||||
{dataInicio ? format(dataInicio, "dd/MM/yyyy") : "Selecionar"}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={dataInicio}
|
||||
onSelect={setDataInicio}
|
||||
initialFocus
|
||||
className="pointer-events-auto"
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-medium">Data Fim</label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"h-8 justify-start text-left font-normal text-xs",
|
||||
!dataFim && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<CalendarIcon className="mr-1 h-3 w-3" />
|
||||
{dataFim ? format(dataFim, "dd/MM/yyyy") : "Selecionar"}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={dataFim}
|
||||
onSelect={setDataFim}
|
||||
initialFocus
|
||||
className="pointer-events-auto"
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{filterOF || filterFase || filterProcesso || dataInicio || dataFim ?
|
||||
'Filtros aplicados automaticamente baseados no último uso' :
|
||||
'Use os filtros para otimizar a visualização'
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
|
||||
import { Package, Wrench, Undo2 } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
import { ApontamentoProducao } from '@/hooks/useApontamentosProducao';
|
||||
|
||||
interface ApontamentosListItemProps {
|
||||
apontamento: ApontamentoProducao;
|
||||
onRevert: (id: string) => void;
|
||||
}
|
||||
|
||||
export const ApontamentosListItem: React.FC<ApontamentosListItemProps> = ({
|
||||
apontamento,
|
||||
onRevert
|
||||
}) => {
|
||||
const formatDate = (dateString: string) => {
|
||||
try {
|
||||
const date = new Date(dateString + 'T00:00:00');
|
||||
return format(date, 'dd/MM/yyyy', { locale: ptBR });
|
||||
} catch (error) {
|
||||
console.error('Erro ao formatar data:', error);
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
const getMarcaItem = (apontamento: ApontamentoProducao) => {
|
||||
if (apontamento.tipo_apontamento === 'componente') {
|
||||
return apontamento.componente?.marca_componente || 'N/A';
|
||||
} else {
|
||||
return apontamento.peca?.marca || 'N/A';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between p-3 border rounded-lg hover:bg-muted/50 transition-colors">
|
||||
<div className="flex items-center gap-3">
|
||||
{apontamento.tipo_apontamento === 'componente' ? (
|
||||
<Wrench className="h-4 w-4 text-blue-500" />
|
||||
) : (
|
||||
<Package className="h-4 w-4 text-green-500" />
|
||||
)}
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">
|
||||
{getMarcaItem(apontamento)}
|
||||
</span>
|
||||
<Badge variant="outline">
|
||||
{apontamento.of_number || 'N/A'}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{apontamento.processo?.nome || 'N/A'} • {formatDate(apontamento.data_apontamento)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-right">
|
||||
<div className="font-medium">
|
||||
{apontamento.quantidade_produzida || 0} un.
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{format(new Date(apontamento.created_at), 'HH:mm', { locale: ptBR })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-orange-600 hover:text-orange-700 hover:bg-orange-50"
|
||||
>
|
||||
<Undo2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Reverter Apontamento</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Tem certeza de que deseja reverter este apontamento?
|
||||
Esta ação não pode ser desfeita e irá remover permanentemente o registro de:
|
||||
<br />
|
||||
<strong>{getMarcaItem(apontamento)}</strong> - {apontamento.quantidade_produzida} unidades
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => onRevert(apontamento.id)}
|
||||
className="bg-orange-600 hover:bg-orange-700"
|
||||
>
|
||||
Confirmar Reversão
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Calendar as CalendarIcon } from 'lucide-react';
|
||||
import { ApontamentosListItem } from './ApontamentosListItem';
|
||||
import { ApontamentoProducao } from '@/hooks/useApontamentosProducao';
|
||||
|
||||
interface ApontamentosResultsListProps {
|
||||
filteredApontamentos: ApontamentoProducao[];
|
||||
onRevert: (id: string) => void;
|
||||
}
|
||||
|
||||
export const ApontamentosResultsList: React.FC<ApontamentosResultsListProps> = ({
|
||||
filteredApontamentos,
|
||||
onRevert
|
||||
}) => {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarIcon className="h-5 w-5" />
|
||||
Histórico de Apontamentos
|
||||
</div>
|
||||
<Badge variant="secondary">
|
||||
{filteredApontamentos.length} {filteredApontamentos.length === 1 ? 'registro' : 'registros'}
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{filteredApontamentos.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<CalendarIcon className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||
<p>Nenhum apontamento encontrado</p>
|
||||
<p className="text-sm">
|
||||
Tente ajustar os filtros para ver mais resultados
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||
{filteredApontamentos.map((apontamento) => (
|
||||
<ApontamentosListItem
|
||||
key={apontamento.id}
|
||||
apontamento={apontamento}
|
||||
onRevert={onRevert}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,273 @@
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { ApontamentoProducao } from '@/hooks/useApontamentosProducao';
|
||||
|
||||
// Cache para lembrar os últimos filtros utilizados
|
||||
const historicCache = {
|
||||
lastOF: '',
|
||||
lastFase: '',
|
||||
lastProcesso: ''
|
||||
};
|
||||
|
||||
export const useApontamentosFilters = (apontamentos: ApontamentoProducao[]) => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [filterOF, setFilterOF] = useState('');
|
||||
const [filterFase, setFilterFase] = useState('');
|
||||
const [filterProcesso, setFilterProcesso] = useState('');
|
||||
const [dataInicio, setDataInicio] = useState<Date | undefined>();
|
||||
const [dataFim, setDataFim] = useState<Date | undefined>();
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
|
||||
// Logs detalhados para debug
|
||||
useEffect(() => {
|
||||
console.log(`🔍 DADOS RECEBIDOS NO FILTRO: ${apontamentos.length} apontamentos`);
|
||||
|
||||
if (apontamentos.length > 0) {
|
||||
// Verificar OF B118 especificamente
|
||||
const b118Records = apontamentos.filter(apt => apt.of_number === 'B118');
|
||||
console.log(`🎯 OF B118 no filtro: ${b118Records.length} registros`);
|
||||
|
||||
// Mostrar alguns exemplos
|
||||
if (b118Records.length > 0) {
|
||||
console.log('🔍 Primeiros 5 registros B118:',
|
||||
b118Records.slice(0, 5).map(apt => ({
|
||||
id: apt.id,
|
||||
of: apt.of_number,
|
||||
peca: apt.peca?.marca,
|
||||
processo: apt.processo?.nome,
|
||||
data: apt.data_apontamento
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [apontamentos]);
|
||||
|
||||
// Carregar cache inicial apenas uma vez
|
||||
useEffect(() => {
|
||||
if (!initialized) {
|
||||
console.log(`📊 Inicializando filtros com ${apontamentos.length} apontamentos totais`);
|
||||
|
||||
const savedCache = localStorage.getItem('historico_apontamentos_cache');
|
||||
if (savedCache) {
|
||||
try {
|
||||
const parsed = JSON.parse(savedCache);
|
||||
Object.assign(historicCache, parsed);
|
||||
console.log('📋 Cache carregado:', historicCache);
|
||||
} catch (error) {
|
||||
console.log('⚠️ Erro ao carregar cache do histórico:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// NÃO aplicar filtros do cache automaticamente - deixar limpo por padrão
|
||||
setInitialized(true);
|
||||
}
|
||||
}, [apontamentos, initialized]);
|
||||
|
||||
// Salvar cache quando filtros mudam
|
||||
const updateCache = (updates: Partial<typeof historicCache>) => {
|
||||
Object.assign(historicCache, updates);
|
||||
localStorage.setItem('historico_apontamentos_cache', JSON.stringify(historicCache));
|
||||
};
|
||||
|
||||
// Obter valores únicos para filtros - SEMPRE todos os valores disponíveis
|
||||
const uniqueOFs = useMemo(() => {
|
||||
const ofs = apontamentos
|
||||
.map(apt => apt.of_number)
|
||||
.filter((of): of is string => {
|
||||
return typeof of === 'string' && of.trim() !== '';
|
||||
})
|
||||
.filter((of, index, array) => array.indexOf(of) === index)
|
||||
.sort();
|
||||
|
||||
console.log(`📋 OFs únicas encontradas: ${ofs.length}`, ofs.slice(0, 10), '...');
|
||||
return ofs;
|
||||
}, [apontamentos]);
|
||||
|
||||
const uniqueFases = useMemo(() => {
|
||||
// Se não há OF selecionada, mostrar todas as fases
|
||||
const apontamentosFiltrados = filterOF && filterOF !== ''
|
||||
? apontamentos.filter(apt => apt.of_number === filterOF)
|
||||
: apontamentos;
|
||||
|
||||
const fases = apontamentosFiltrados
|
||||
.map(apt => apt.peca?.etapa_fase)
|
||||
.filter((fase): fase is string => {
|
||||
return typeof fase === 'string' && fase.trim() !== '';
|
||||
})
|
||||
.filter((fase, index, array) => array.indexOf(fase) === index)
|
||||
.sort();
|
||||
|
||||
console.log(`📋 Fases únicas encontradas para OF "${filterOF}": ${fases.length}`, fases);
|
||||
return fases;
|
||||
}, [apontamentos, filterOF]);
|
||||
|
||||
const uniqueProcessos = useMemo(() => {
|
||||
// Aplicar filtros hierárquicos apenas se existirem
|
||||
let apontamentosFiltrados = apontamentos;
|
||||
|
||||
if (filterOF && filterOF !== '') {
|
||||
apontamentosFiltrados = apontamentosFiltrados.filter(apt => apt.of_number === filterOF);
|
||||
}
|
||||
|
||||
if (filterFase && filterFase !== '') {
|
||||
apontamentosFiltrados = apontamentosFiltrados.filter(apt => apt.peca?.etapa_fase === filterFase);
|
||||
}
|
||||
|
||||
const processos = apontamentosFiltrados
|
||||
.map(apt => apt.processo?.nome)
|
||||
.filter((processo): processo is string => {
|
||||
return typeof processo === 'string' && processo.trim() !== '';
|
||||
})
|
||||
.filter((processo, index, array) => array.indexOf(processo) === index)
|
||||
.sort();
|
||||
|
||||
console.log(`📋 Processos únicos encontrados para OF "${filterOF}" e Fase "${filterFase}": ${processos.length}`, processos);
|
||||
return processos;
|
||||
}, [apontamentos, filterOF, filterFase]);
|
||||
|
||||
// Filtrar apontamentos - LÓGICA SIMPLIFICADA E CORRIGIDA
|
||||
const filteredApontamentos = useMemo(() => {
|
||||
if (!initialized) {
|
||||
console.log('⏳ Aguardando inicialização...');
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log(`🔍 INICIANDO FILTRAGEM DE ${apontamentos.length} APONTAMENTOS`);
|
||||
console.log('🔍 Filtros ativos:', {
|
||||
searchTerm: searchTerm || 'VAZIO',
|
||||
filterOF: filterOF || 'VAZIO',
|
||||
filterFase: filterFase || 'VAZIO',
|
||||
filterProcesso: filterProcesso || 'VAZIO',
|
||||
dataInicio: dataInicio ? 'SIM' : 'NÃO',
|
||||
dataFim: dataFim ? 'SIM' : 'NÃO'
|
||||
});
|
||||
|
||||
let resultado = [...apontamentos]; // Começar com TODOS os apontamentos
|
||||
|
||||
// Aplicar filtro de busca textual APENAS se preenchido
|
||||
if (searchTerm && searchTerm.trim() !== '') {
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
const beforeSearch = resultado.length;
|
||||
|
||||
resultado = resultado.filter(apt =>
|
||||
(apt.of_number && apt.of_number.toLowerCase().includes(searchLower)) ||
|
||||
(apt.peca?.marca && apt.peca.marca.toLowerCase().includes(searchLower)) ||
|
||||
(apt.componente?.marca_componente && apt.componente.marca_componente.toLowerCase().includes(searchLower))
|
||||
);
|
||||
|
||||
console.log(`🔎 Filtro texto "${searchTerm}": ${beforeSearch} → ${resultado.length} registros`);
|
||||
}
|
||||
|
||||
// Aplicar filtro por OF APENAS se selecionado
|
||||
if (filterOF && filterOF !== '' && filterOF !== 'all') {
|
||||
const beforeOF = resultado.length;
|
||||
resultado = resultado.filter(apt => apt.of_number === filterOF);
|
||||
console.log(`🏷️ Filtro OF "${filterOF}": ${beforeOF} → ${resultado.length} registros`);
|
||||
}
|
||||
|
||||
// Aplicar filtro por Fase APENAS se selecionado
|
||||
if (filterFase && filterFase !== '' && filterFase !== 'all') {
|
||||
const beforeFase = resultado.length;
|
||||
resultado = resultado.filter(apt => apt.peca?.etapa_fase === filterFase);
|
||||
console.log(`📋 Filtro Fase "${filterFase}": ${beforeFase} → ${resultado.length} registros`);
|
||||
}
|
||||
|
||||
// Aplicar filtro por Processo APENAS se selecionado
|
||||
if (filterProcesso && filterProcesso !== '' && filterProcesso !== 'all') {
|
||||
const beforeProcesso = resultado.length;
|
||||
resultado = resultado.filter(apt => apt.processo?.nome === filterProcesso);
|
||||
console.log(`⚙️ Filtro Processo "${filterProcesso}": ${beforeProcesso} → ${resultado.length} registros`);
|
||||
}
|
||||
|
||||
// Aplicar filtros de data APENAS se definidos
|
||||
if (dataInicio || dataFim) {
|
||||
const beforeData = resultado.length;
|
||||
|
||||
resultado = resultado.filter(apt => {
|
||||
const apontamentoDate = new Date(apt.data_apontamento);
|
||||
let matchesDataInicio = true;
|
||||
let matchesDataFim = true;
|
||||
|
||||
if (dataInicio) {
|
||||
matchesDataInicio = apontamentoDate >= dataInicio;
|
||||
}
|
||||
if (dataFim) {
|
||||
matchesDataFim = apontamentoDate <= dataFim;
|
||||
}
|
||||
|
||||
return matchesDataInicio && matchesDataFim;
|
||||
});
|
||||
|
||||
console.log(`📅 Filtro Data: ${beforeData} → ${resultado.length} registros`);
|
||||
}
|
||||
|
||||
console.log(`✅ RESULTADO FINAL DA FILTRAGEM: ${resultado.length} de ${apontamentos.length} apontamentos`);
|
||||
|
||||
// Log específico para B118 se estiver sendo filtrado
|
||||
if (filterOF === 'B118') {
|
||||
console.log(`🎯 Resultado B118: ${resultado.length} registros filtrados`);
|
||||
}
|
||||
|
||||
return resultado;
|
||||
}, [apontamentos, searchTerm, filterOF, filterFase, filterProcesso, dataInicio, dataFim, initialized]);
|
||||
|
||||
const clearFilters = () => {
|
||||
console.log('🧹 Limpando todos os filtros');
|
||||
setSearchTerm('');
|
||||
setFilterOF('');
|
||||
setFilterFase('');
|
||||
setFilterProcesso('');
|
||||
setDataInicio(undefined);
|
||||
setDataFim(undefined);
|
||||
|
||||
// Limpar cache
|
||||
Object.assign(historicCache, { lastOF: '', lastFase: '', lastProcesso: '' });
|
||||
localStorage.removeItem('historico_apontamentos_cache');
|
||||
};
|
||||
|
||||
const handleOFChange = (value: string) => {
|
||||
const selectedValue = value === 'all' ? '' : value;
|
||||
console.log('🔄 Mudando OF para:', selectedValue);
|
||||
setFilterOF(selectedValue);
|
||||
// Limpar fase e processo quando OF muda
|
||||
setFilterFase('');
|
||||
setFilterProcesso('');
|
||||
updateCache({ lastOF: selectedValue, lastFase: '', lastProcesso: '' });
|
||||
};
|
||||
|
||||
const handleFaseChange = (value: string) => {
|
||||
const selectedValue = value === 'all' ? '' : value;
|
||||
console.log('🔄 Mudando Fase para:', selectedValue);
|
||||
setFilterFase(selectedValue);
|
||||
// Limpar processo quando fase muda
|
||||
setFilterProcesso('');
|
||||
updateCache({ lastFase: selectedValue, lastProcesso: '' });
|
||||
};
|
||||
|
||||
const handleProcessoChange = (value: string) => {
|
||||
const selectedValue = value === 'all' ? '' : value;
|
||||
console.log('🔄 Mudando Processo para:', selectedValue);
|
||||
setFilterProcesso(selectedValue);
|
||||
updateCache({ lastProcesso: selectedValue });
|
||||
};
|
||||
|
||||
return {
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
filterOF,
|
||||
filterFase,
|
||||
filterProcesso,
|
||||
dataInicio,
|
||||
dataFim,
|
||||
setDataInicio,
|
||||
setDataFim,
|
||||
uniqueOFs,
|
||||
uniqueFases,
|
||||
uniqueProcessos,
|
||||
filteredApontamentos,
|
||||
initialized,
|
||||
handleOFChange,
|
||||
handleFaseChange,
|
||||
handleProcessoChange,
|
||||
clearFilters
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user