🚀 Initial commit: Versão atual do TrackSteel APP
This commit is contained in:
305
src/components/admin/CleanupDuplicatesModal.tsx
Normal file
305
src/components/admin/CleanupDuplicatesModal.tsx
Normal file
@@ -0,0 +1,305 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Loader2, Search, AlertTriangle, Trash2 } from 'lucide-react';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface CleanupDuplicatesModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface DuplicateGroup {
|
||||
chave_agrupamento: string;
|
||||
of_number: string;
|
||||
marca: string;
|
||||
etapa_fase: string;
|
||||
processo_nome: string;
|
||||
quantidade_total_peca: number;
|
||||
apontamentos: Array<{
|
||||
id: string;
|
||||
data_apontamento: string;
|
||||
created_at: string;
|
||||
quantidade_produzida: number;
|
||||
tipo_apontamento: string;
|
||||
}>;
|
||||
total_apontado: number;
|
||||
excesso: number;
|
||||
}
|
||||
|
||||
export const CleanupDuplicatesModal: React.FC<CleanupDuplicatesModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
}) => {
|
||||
const [ofNumber, setOfNumber] = useState('');
|
||||
const [duplicatesData, setDuplicatesData] = useState<{
|
||||
duplicatesFound: boolean;
|
||||
details: DuplicateGroup[];
|
||||
totalGroups: number;
|
||||
groupsWithDuplicates: number;
|
||||
} | null>(null);
|
||||
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
||||
const [isCleaning, setIsCleaning] = useState(false);
|
||||
|
||||
const analyzeOF = async () => {
|
||||
if (!ofNumber.trim()) {
|
||||
toast.error('Por favor, informe o número da OF');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsAnalyzing(true);
|
||||
try {
|
||||
const { data, error } = await supabase.functions.invoke('cleanup-duplicates', {
|
||||
body: {
|
||||
of_number: ofNumber.trim(),
|
||||
action: 'analyze' // Apenas analisar, não executar limpeza
|
||||
}
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
setDuplicatesData({
|
||||
duplicatesFound: data.groupsWithDuplicates > 0,
|
||||
details: data.details || [],
|
||||
totalGroups: data.totalGroups || 0,
|
||||
groupsWithDuplicates: data.groupsWithDuplicates || 0
|
||||
});
|
||||
|
||||
if (data.groupsWithDuplicates === 0) {
|
||||
toast.success('Nenhuma duplicata encontrada para esta OF!');
|
||||
} else {
|
||||
toast.info(`Encontradas ${data.groupsWithDuplicates} duplicatas para análise`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao analisar duplicatas:', error);
|
||||
toast.error('Erro ao analisar duplicatas');
|
||||
} finally {
|
||||
setIsAnalyzing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const executeCleaning = async () => {
|
||||
setIsCleaning(true);
|
||||
try {
|
||||
const { data, error } = await supabase.functions.invoke('cleanup-duplicates', {
|
||||
body: {
|
||||
of_number: ofNumber.trim(),
|
||||
action: 'execute' // Executar limpeza
|
||||
}
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
toast.success(`Limpeza concluída! ${data.duplicatesRemoved} duplicatas removidas.`);
|
||||
|
||||
// Resetar dados após limpeza
|
||||
setDuplicatesData(null);
|
||||
setOfNumber('');
|
||||
} catch (error) {
|
||||
console.error('Erro ao executar limpeza:', error);
|
||||
toast.error('Erro ao executar limpeza de duplicatas');
|
||||
} finally {
|
||||
setIsCleaning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setDuplicatesData(null);
|
||||
setOfNumber('');
|
||||
onClose();
|
||||
};
|
||||
|
||||
const groupedByPhase = duplicatesData?.details.reduce((acc, group) => {
|
||||
const phase = group.etapa_fase;
|
||||
if (!acc[phase]) acc[phase] = [];
|
||||
acc[phase].push(group);
|
||||
return acc;
|
||||
}, {} as Record<string, DuplicateGroup[]>) || {};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<AlertTriangle className="w-5 h-5 text-orange-500" />
|
||||
Limpeza de Duplicatas de Apontamentos
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Input para OF */}
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Digite o número da OF (ex: B117)"
|
||||
value={ofNumber}
|
||||
onChange={(e) => setOfNumber(e.target.value.toUpperCase())}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
onClick={analyzeOF}
|
||||
disabled={isAnalyzing || !ofNumber.trim()}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{isAnalyzing ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Search className="w-4 h-4" />
|
||||
)}
|
||||
Analisar
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Resultados da análise */}
|
||||
{duplicatesData && (
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">
|
||||
Resumo da Análise - OF {ofNumber}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
{duplicatesData.totalGroups}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Grupos Analisados
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-orange-600">
|
||||
{duplicatesData.groupsWithDuplicates}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Com Duplicatas
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-red-600">
|
||||
{duplicatesData.details.reduce((sum, g) => sum + g.apontamentos.length - 1, 0)}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Registros a Remover
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Detalhes por fase */}
|
||||
{duplicatesData.duplicatesFound && (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">Duplicatas Encontradas por Fase:</h3>
|
||||
|
||||
{Object.entries(groupedByPhase).map(([phase, groups]) => (
|
||||
<Card key={phase}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
Fase {phase}
|
||||
<Badge variant="destructive">
|
||||
{groups.length} duplicatas
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{groups.map((group, index) => (
|
||||
<div key={index} className="border rounded-lg p-3 bg-muted/30">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div>
|
||||
<h4 className="font-medium">
|
||||
Marca: {group.marca} | Processo: {group.processo_nome}
|
||||
</h4>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Quantidade da Peça: {group.quantidade_total_peca} |
|
||||
Total Apontado: {group.total_apontado}
|
||||
{group.excesso > 0 && (
|
||||
<span className="text-red-600 font-medium">
|
||||
{' '}| Excesso: +{group.excesso}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline">
|
||||
{group.apontamentos.length} apontamentos
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
{group.apontamentos.map((apt, aptIndex) => (
|
||||
<div
|
||||
key={aptIndex}
|
||||
className={`p-2 rounded text-xs border ${
|
||||
aptIndex === 0
|
||||
? 'bg-green-50 border-green-200'
|
||||
: 'bg-red-50 border-red-200'
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium">
|
||||
{aptIndex === 0 ? '✅ Manter' : '🗑️ Remover'}
|
||||
</div>
|
||||
<div>Data: {new Date(apt.data_apontamento).toLocaleDateString('pt-BR')}</div>
|
||||
<div>Qtd: {apt.quantidade_produzida}</div>
|
||||
<div>Criado: {new Date(apt.created_at).toLocaleDateString('pt-BR')}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{/* Botão para executar limpeza */}
|
||||
<div className="flex justify-end gap-2 pt-4 border-t">
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button
|
||||
onClick={executeCleaning}
|
||||
disabled={isCleaning}
|
||||
className="bg-red-600 hover:bg-red-700 flex items-center gap-2"
|
||||
>
|
||||
{isCleaning ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="w-4 h-4" />
|
||||
)}
|
||||
Executar Limpeza Definitiva
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!duplicatesData.duplicatesFound && (
|
||||
<Card>
|
||||
<CardContent className="text-center py-8">
|
||||
<div className="text-green-600 mb-2">✅</div>
|
||||
<h3 className="font-medium mb-2">Nenhuma Duplicata Encontrada</h3>
|
||||
<p className="text-muted-foreground">
|
||||
A OF {ofNumber} está livre de duplicatas de apontamentos.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user