🚀 Initial commit: Versão atual do TrackSteel APP

This commit is contained in:
2026-03-18 21:17:53 +00:00
commit bde410c9ad
633 changed files with 108150 additions and 0 deletions
@@ -0,0 +1,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
};
};