Files
tracksteel_app/src/components/admin/CleanupDuplicatesModal.tsx

306 lines
11 KiB
TypeScript

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>
);
};