413 lines
16 KiB
TypeScript
413 lines
16 KiB
TypeScript
|
|
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>
|
|
);
|
|
};
|