diff --git a/src/components/apontamento/ApontamentoForm.tsx b/src/components/apontamento/ApontamentoForm.tsx index e7c8772..9a46664 100644 --- a/src/components/apontamento/ApontamentoForm.tsx +++ b/src/components/apontamento/ApontamentoForm.tsx @@ -19,6 +19,7 @@ interface ItemDisponivel { id: string; marca: string; descricao: string; + perfil?: string; tipo: 'peca' | 'componente'; quantidade_disponivel: number; processo_atual_permitido: number; @@ -161,7 +162,8 @@ export const ApontamentoForm = () => { pecasDisponiveis.push({ id: peca.id, marca: peca.marca, - descricao: peca.descricao || '', + descricao: peca.descricao || peca.perfil_principal || '', + perfil: peca.perfil_principal || peca.descricao || '', tipo: 'peca', quantidade_disponivel: saldoDisponivel, processo_atual_permitido: ordemProcesso, @@ -197,6 +199,7 @@ export const ApontamentoForm = () => { id: comp.componente_ids[0] || '', marca: comp.marca_componente, descricao: comp.descricao || comp.perfil || '', + perfil: comp.perfil || comp.descricao || '', tipo: 'componente', quantidade_disponivel: saldoComp, processo_atual_permitido: ordemProcesso, @@ -364,6 +367,54 @@ export const ApontamentoForm = () => { })); }; + const handleApontarItemDireto = async (item: ItemDisponivel, quantidade: number): Promise => { + if (!formData.processo_id) { + toast.error('Selecione um processo antes de apontar'); + return false; + } + + if (isNaN(quantidade) || quantidade <= 0 || quantidade > item.quantidade_disponivel) { + toast.error(`Quantidade inválida. Saldo disponível: ${item.quantidade_disponivel}`); + return false; + } + + setSaving(true); + try { + const apontamentoData: Parameters[0] = { + of_number: formData.of_number, + tipo_apontamento: item.tipo, + processo_id: formData.processo_id, + quantidade_produzida: quantidade, + data_apontamento: formData.data_apontamento, + observacoes: formData.observacoes || undefined + }; + + if (item.tipo === 'componente') { + apontamentoData.componente_id = item.id; + } else { + apontamentoData.peca_id = item.id; + } + + const result = await criarApontamento(apontamentoData); + + if (result.success) { + toast.success(`${quantidade} un de ${item.marca} apontada(s) com sucesso!`); + await refetch(); + resetFormForNewEntry(); + return true; + } else { + toast.error('Erro ao registrar apontamento'); + return false; + } + } catch (error) { + console.error('Erro no apontamento direto:', error); + toast.error('Erro ao registrar apontamento'); + return false; + } finally { + setSaving(false); + } + }; + const resetFormForNewEntry = () => { setItemSelecionado(null); setFormData(prev => ({ @@ -560,6 +611,7 @@ export const ApontamentoForm = () => { itemSelecionado={itemSelecionado} onItemSelect={handleItemSelect} onBatchSelect={handleBatchSelect} + onApontarItemDireto={handleApontarItemDireto} loading={isLoadingItens} /> diff --git a/src/components/apontamento/SeletorItensOtimizado.tsx b/src/components/apontamento/SeletorItensOtimizado.tsx index cb1aece..8c40b76 100644 --- a/src/components/apontamento/SeletorItensOtimizado.tsx +++ b/src/components/apontamento/SeletorItensOtimizado.tsx @@ -4,7 +4,7 @@ import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { ScrollArea } from "@/components/ui/scroll-area"; import { toast } from 'sonner'; -import { Search, Package, Shield, AlertTriangle, Settings } from 'lucide-react'; +import { Search, Package, Shield, AlertTriangle, Settings, Check, Loader2 } from 'lucide-react'; import { useDebounce } from '@/hooks/useDebounce'; import { Peca } from '@/hooks/usePecas'; import { useComponentesPeca } from '@/hooks/useComponentesPeca'; @@ -19,10 +19,11 @@ import { DialogTitle, } from "@/components/ui/dialog"; -interface ItemDisponivel { +export interface ItemDisponivel { id: string; marca: string; descricao: string; + perfil?: string; tipo: 'peca' | 'componente'; quantidade_disponivel: number; processo_atual_permitido: number; @@ -54,6 +55,7 @@ interface SeletorItensOtimizadoProps { itemSelecionado?: ItemDisponivel | null; onItemSelect?: (item: ItemDisponivel) => void; onBatchSelect?: (items: ItemDisponivel[], tipo: 'peca' | 'componente') => Promise; + onApontarItemDireto?: (item: ItemDisponivel, quantidade: number) => Promise; loading?: boolean; onSelectPeca?: (peca: PecaWithComponents) => void; onSelectComponente?: (componente: ComponenteItemData) => void; @@ -71,6 +73,7 @@ export const SeletorItensOtimizado: React.FC = ({ itemSelecionado, onItemSelect, onBatchSelect, + onApontarItemDireto, loading = false, onSelectPeca, onSelectComponente, @@ -83,6 +86,11 @@ export const SeletorItensOtimizado: React.FC = ({ const [loadingPecas, setLoadingPecas] = useState(false); const [loadingComponentes, setLoadingComponentes] = useState(false); const [componentes, setComponentes] = useState([]); + + // Estados locais para apontamento direto por card (quantidade, modo todas e loading) + const [qtdCards, setQtdCards] = useState>({}); + const [todasCards, setTodasCards] = useState>({}); + const [loadingApontandoId, setLoadingApontandoId] = useState(null); const [showComponentes, setShowComponentes] = useState(false); const inputRef = useRef(null); @@ -153,16 +161,81 @@ export const SeletorItensOtimizado: React.FC = ({ const pecasOrdenadas = useMemo(() => { let pecasFiltradas = pecasDisponiveis; if (filtroNumeroPeca) { + const termo = filtroNumeroPeca.toLowerCase().trim(); pecasFiltradas = pecasDisponiveis.filter(peca => - peca.marca.toLowerCase().includes(filtroNumeroPeca.toLowerCase()) + peca.marca.toLowerCase().includes(termo) || + (peca.perfil && peca.perfil.toLowerCase().includes(termo)) || + (peca.descricao && peca.descricao.toLowerCase().includes(termo)) ); } return pecasFiltradas.sort((a, b) => a.marca.localeCompare(b.marca, undefined, { numeric: true, sensitivity: 'base' })); }, [pecasDisponiveis, filtroNumeroPeca]); const componentesOrdenados = useMemo(() => { - return componentesDisponiveis.sort((a, b) => a.marca.localeCompare(b.marca, undefined, { numeric: true, sensitivity: 'base' })); - }, [componentesDisponiveis]); + let comps = componentesDisponiveis; + if (filtroNumeroPeca) { + const termo = filtroNumeroPeca.toLowerCase().trim(); + comps = componentesDisponiveis.filter(comp => + comp.marca.toLowerCase().includes(termo) || + (comp.perfil && comp.perfil.toLowerCase().includes(termo)) || + (comp.descricao && comp.descricao.toLowerCase().includes(termo)) + ); + } + return comps.sort((a, b) => a.marca.localeCompare(b.marca, undefined, { numeric: true, sensitivity: 'base' })); + }, [componentesDisponiveis, filtroNumeroPeca]); + + const handleQtdCardChange = useCallback((itemId: string, value: string) => { + setQtdCards(prev => ({ ...prev, [itemId]: value })); + setTodasCards(prev => ({ ...prev, [itemId]: false })); + }, []); + + const handleToggleTodas = useCallback((itemId: string, qtdDisponivel: number) => { + setTodasCards(prev => { + const novoEstado = !prev[itemId]; + if (novoEstado) { + setQtdCards(q => ({ ...q, [itemId]: qtdDisponivel.toString() })); + } else { + setQtdCards(q => ({ ...q, [itemId]: '' })); + } + return { ...prev, [itemId]: novoEstado }; + }); + }, []); + + const handleExecutarBaixa = useCallback(async (item: ItemDisponivel) => { + if (!onApontarItemDireto) return; + const isTodas = todasCards[item.id]; + const qtdDigitada = qtdCards[item.id]; + const quantidade = isTodas ? item.quantidade_disponivel : parseInt(qtdDigitada); + + if (isNaN(quantidade) || quantidade <= 0) { + toast.error(`Informe a quantidade para ${item.marca} ou ative 'Todas'`); + return; + } + + if (quantidade > item.quantidade_disponivel) { + toast.error(`Quantidade não pode ser maior que ${item.quantidade_disponivel} disponível`); + return; + } + + setLoadingApontandoId(item.id); + try { + const sucesso = await onApontarItemDireto(item, quantidade); + if (sucesso) { + setQtdCards(prev => { + const copia = { ...prev }; + delete copia[item.id]; + return copia; + }); + setTodasCards(prev => { + const copia = { ...prev }; + delete copia[item.id]; + return copia; + }); + } + } finally { + setLoadingApontandoId(null); + } + }, [onApontarItemDireto, todasCards, qtdCards]); const fetchPecas = useCallback(async (term: string) => { if (!term.trim()) { @@ -298,50 +371,120 @@ export const SeletorItensOtimizado: React.FC = ({ ); - const ItemDisponivelComponent = React.memo(({ item, isSelected }: { item: ItemDisponivel; isSelected: boolean }) => ( -
-
handleItemSelect(item)} - > -
-
-
{item.marca}
-
- {item.quantidade_disponivel} disponível + const ItemDisponivelComponent = ({ item, isSelected }: { item: ItemDisponivel; isSelected: boolean }) => { + const isTodas = Boolean(todasCards[item.id]); + const qtdValue = qtdCards[item.id] ?? (isTodas ? item.quantidade_disponivel.toString() : ''); + const isLoading = loadingApontandoId === item.id; + + return ( +
+
handleItemSelect(item)} + > +
+
+
+ {item.marca} + {item.perfil && ( + + {item.perfil} + + )} +
+
+ {item.quantidade_disponivel} disponível +
-
- {item.descricao && ( -
- {item.descricao} + + {item.descricao && item.descricao !== item.perfil && ( +
+ {item.descricao} +
+ )} + + {/* Linha com processo e controles rápidos de apontamento desenhados pelo usuário */} +
+
+ Processo: {item.nome_processo || item.processo_atual_permitido} +
+ + {onApontarItemDireto && ( +
e.stopPropagation()} + > + handleQtdCardChange(item.id, e.target.value)} + disabled={isTodas || isLoading} + className="h-7 w-20 text-xs px-2 text-center font-medium bg-background border-input shadow-none" + /> + + + + +
+ )}
- )} -
- Processo: {item.nome_processo || item.processo_atual_permitido}
-
- {isAdmin && ( - - )} -
- )); + {isAdmin && ( + + )} +
+ ); + }; if (onSelectPeca) { return ( @@ -418,10 +561,10 @@ export const SeletorItensOtimizado: React.FC = ({
setFiltroNumeroPeca(e.target.value)} - className="bg-background border-input text-foreground placeholder:text-muted-foreground h-8 w-48 shadow-sm" + className="bg-background border-input text-foreground placeholder:text-muted-foreground h-8 w-56 shadow-sm text-xs" />