arcadiasuite/client/src/pages/QualityModule.tsx

1723 lines
80 KiB
TypeScript

import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { BrowserFrame } from "@/components/Browser/BrowserFrame";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { ScrollArea } from "@/components/ui/scroll-area";
import { useToast } from "@/hooks/use-toast";
import {
Beaker, FileText, AlertTriangle, ClipboardList, Users, MapPin,
Plus, Search, Edit, Trash2, RefreshCw, CheckCircle, XCircle, Clock,
Building2, FlaskConical, FileCheck, GraduationCap, Loader2, Eye,
Upload, Download, FileUp, BookOpen, Calendar, UserCheck, Award
} from "lucide-react";
type TabType = "dashboard" | "samples" | "lab-reports" | "rnc" | "documents" | "field-forms" | "training" | "suppliers";
interface Sample {
id: number;
sampleCode: string;
sampleType: string;
collectionDate: string;
collectionLocation: string;
collectionResponsible: string;
laboratoryId?: number;
status: string;
projectId?: number;
}
interface LabReport {
id: number;
sampleId: number;
reportNumber: string;
laboratoryId?: number;
issueDate: string;
status: string;
conclusion?: string;
}
interface NonConformity {
id: number;
rncNumber: string;
title: string;
description: string;
type: string;
severity: string;
status: string;
detectedAt: string;
detectedBy: string;
dueDate?: string;
}
interface QualityDocument {
id: number;
documentCode: string;
title: string;
type: string;
category: string;
version: string;
status: string;
effectiveDate?: string;
}
interface FieldForm {
id: number;
formCode: string;
formType: string;
title: string;
collectionDate: string;
location: string;
status: string;
}
interface Training {
id: number;
employeeId: string;
trainingName: string;
trainingType: string;
completedDate: string;
expiryDate: string;
status: string;
}
interface HomologatedSupplier {
id: number;
code: string;
name: string;
certifications: string[];
qualityScore: number;
homologationStatus: string;
homologationExpiry: string;
}
interface DashboardData {
samples: { total: number };
nonConformities: { open: number };
expenses: { pending: number; totalPending: number };
documents: { active: number };
trainings: { expiringSoon: number };
}
const api = {
get: async (url: string) => {
const res = await fetch(url, { credentials: "include" });
if (!res.ok) throw new Error("Request failed");
return res.json();
},
post: async (url: string, data: any) => {
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
credentials: "include",
});
if (!res.ok) throw new Error("Request failed");
return res.json();
},
put: async (url: string, data: any) => {
const res = await fetch(url, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
credentials: "include",
});
if (!res.ok) throw new Error("Request failed");
return res.json();
},
};
const statusColors: Record<string, string> = {
coletada: "bg-blue-500",
enviada: "bg-yellow-500",
em_analise: "bg-orange-500",
resultado_recebido: "bg-purple-500",
aprovada: "bg-green-500",
reprovada: "bg-red-500",
aberta: "bg-red-500",
em_tratamento: "bg-yellow-500",
pendente_verificacao: "bg-orange-500",
fechada: "bg-green-500",
vigente: "bg-green-500",
em_revisao: "bg-yellow-500",
obsoleto: "bg-gray-500",
rascunho: "bg-gray-400",
preenchido: "bg-blue-500",
revisado: "bg-purple-500",
aprovado: "bg-green-500",
pendente: "bg-yellow-500",
em_andamento: "bg-blue-500",
concluido: "bg-green-500",
vencido: "bg-red-500",
approved: "bg-green-500",
pending: "bg-yellow-500",
expired: "bg-red-500",
blocked: "bg-red-700",
};
export default function QualityModule() {
const { toast } = useToast();
const queryClient = useQueryClient();
const [activeTab, setActiveTab] = useState<TabType>("dashboard");
const [searchTerm, setSearchTerm] = useState("");
const [showSampleDialog, setShowSampleDialog] = useState(false);
const [showRncDialog, setShowRncDialog] = useState(false);
const [showDocDialog, setShowDocDialog] = useState(false);
const [showDocViewDialog, setShowDocViewDialog] = useState(false);
const [selectedDoc, setSelectedDoc] = useState<QualityDocument | null>(null);
const [editingItem, setEditingItem] = useState<any>(null);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [viewingFileUrl, setViewingFileUrl] = useState<string | null>(null);
const [showTrainingDialog, setShowTrainingDialog] = useState(false);
const [editingTraining, setEditingTraining] = useState<Training | null>(null);
const [showTrainingViewDialog, setShowTrainingViewDialog] = useState(false);
const [selectedTraining, setSelectedTraining] = useState<any>(null);
const sampleTrainings = [
{ id: 1, employeeName: "João Silva", employeeId: "emp-001", trainingName: "Amostragem de Água Subterrânea", trainingType: "Técnico", completedDate: "2025-10-15", expiryDate: "2026-10-15", status: "valid", instructor: "Dr. Roberto Lima", workload: 16 },
{ id: 2, employeeName: "Maria Santos", employeeId: "emp-002", trainingName: "NR-35 Trabalho em Altura", trainingType: "Segurança", completedDate: "2025-08-20", expiryDate: "2026-08-20", status: "valid", instructor: "SENAI", workload: 8 },
{ id: 3, employeeName: "Carlos Oliveira", employeeId: "emp-003", trainingName: "Operação de Sondas", trainingType: "Técnico", completedDate: "2024-12-10", expiryDate: "2025-12-10", status: "expiring", instructor: "Técnico Sr. Paulo", workload: 40 },
{ id: 4, employeeName: "Ana Costa", employeeId: "emp-004", trainingName: "ISO 17025 - Requisitos", trainingType: "Qualidade", completedDate: "2024-06-05", expiryDate: "2025-06-05", status: "expired", instructor: "Bureau Veritas", workload: 24 },
];
const { data: dashboard, isLoading: loadingDashboard } = useQuery<DashboardData>({
queryKey: ["/api/quality/dashboard"],
queryFn: () => api.get("/api/quality/dashboard"),
});
const { data: samples = [], refetch: refetchSamples } = useQuery<Sample[]>({
queryKey: ["/api/quality/samples"],
queryFn: async () => {
const res = await api.get("/api/quality/samples");
return res.data || [];
},
});
const { data: labReports = [] } = useQuery<LabReport[]>({
queryKey: ["/api/quality/lab-reports"],
queryFn: async () => {
const res = await api.get("/api/quality/lab-reports");
return res.data || [];
},
});
const { data: nonConformities = [], refetch: refetchRnc } = useQuery<NonConformity[]>({
queryKey: ["/api/quality/non-conformities"],
queryFn: async () => {
const res = await api.get("/api/quality/non-conformities");
return res.data || [];
},
});
const { data: documents = [], refetch: refetchDocs } = useQuery<QualityDocument[]>({
queryKey: ["/api/quality/documents"],
queryFn: async () => {
const res = await api.get("/api/quality/documents");
return res.data || [];
},
});
const { data: fieldForms = [] } = useQuery<FieldForm[]>({
queryKey: ["/api/quality/field-forms"],
queryFn: async () => {
const res = await api.get("/api/quality/field-forms");
return res.data || [];
},
});
const { data: trainings = [] } = useQuery<Training[]>({
queryKey: ["/api/quality/training-matrix"],
queryFn: async () => {
const res = await api.get("/api/quality/training-matrix");
return res.data || [];
},
});
const { data: homologatedSuppliers = [] } = useQuery<HomologatedSupplier[]>({
queryKey: ["/api/quality/homologated-suppliers"],
queryFn: async () => {
const res = await api.get("/api/quality/homologated-suppliers");
return res.data || [];
},
});
const createSampleMutation = useMutation({
mutationFn: (data: Partial<Sample>) => api.post("/api/quality/samples", data),
onSuccess: () => {
toast({ title: "Amostra criada com sucesso" });
refetchSamples();
setShowSampleDialog(false);
},
onError: () => toast({ title: "Erro ao criar amostra", variant: "destructive" }),
});
const createRncMutation = useMutation({
mutationFn: (data: Partial<NonConformity>) => api.post("/api/quality/non-conformities", data),
onSuccess: () => {
toast({ title: "RNC criada com sucesso" });
refetchRnc();
setShowRncDialog(false);
},
onError: () => toast({ title: "Erro ao criar RNC", variant: "destructive" }),
});
const createDocMutation = useMutation({
mutationFn: (data: Partial<QualityDocument>) => api.post("/api/quality/documents", data),
onSuccess: () => {
toast({ title: "Documento criado com sucesso" });
refetchDocs();
setShowDocDialog(false);
},
onError: () => toast({ title: "Erro ao criar documento", variant: "destructive" }),
});
const getStatusBadge = (status: string) => {
const color = statusColors[status] || "bg-gray-500";
const label = status.replace(/_/g, " ").replace(/^\w/, c => c.toUpperCase());
return <Badge className={`${color} text-white`}>{label}</Badge>;
};
const filteredSamples = samples.filter(s =>
s.sampleCode?.toLowerCase().includes(searchTerm.toLowerCase()) ||
s.collectionLocation?.toLowerCase().includes(searchTerm.toLowerCase())
);
const filteredRnc = nonConformities.filter(r =>
r.rncNumber?.toLowerCase().includes(searchTerm.toLowerCase()) ||
r.title?.toLowerCase().includes(searchTerm.toLowerCase())
);
const filteredDocs = documents.filter(d =>
d.documentCode?.toLowerCase().includes(searchTerm.toLowerCase()) ||
d.title?.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<BrowserFrame>
<div className="p-6 space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold flex items-center gap-2">
<Beaker className="h-6 w-6 text-green-600" />
Gestão da Qualidade - ISO 17025
</h1>
<p className="text-muted-foreground">
Controle de amostras, laudos, documentos e conformidade
</p>
</div>
</div>
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as TabType)}>
<TabsList className="h-12 flex-wrap">
<TabsTrigger value="dashboard" className="gap-2" data-testid="tab-quality-dashboard">
<ClipboardList className="h-4 w-4" /> Dashboard
</TabsTrigger>
<TabsTrigger value="samples" className="gap-2" data-testid="tab-samples">
<FlaskConical className="h-4 w-4" /> Amostras
</TabsTrigger>
<TabsTrigger value="lab-reports" className="gap-2" data-testid="tab-lab-reports">
<FileCheck className="h-4 w-4" /> Laudos
</TabsTrigger>
<TabsTrigger value="rnc" className="gap-2" data-testid="tab-rnc">
<AlertTriangle className="h-4 w-4" /> RNC
</TabsTrigger>
<TabsTrigger value="documents" className="gap-2" data-testid="tab-qms-docs">
<FileText className="h-4 w-4" /> Documentos QMS
</TabsTrigger>
<TabsTrigger value="field-forms" className="gap-2" data-testid="tab-field-forms">
<MapPin className="h-4 w-4" /> Formulários Campo
</TabsTrigger>
<TabsTrigger value="training" className="gap-2" data-testid="tab-training">
<GraduationCap className="h-4 w-4" /> Treinamentos
</TabsTrigger>
<TabsTrigger value="suppliers" className="gap-2" data-testid="tab-homologation">
<Building2 className="h-4 w-4" /> Homologação
</TabsTrigger>
</TabsList>
<TabsContent value="dashboard" className="space-y-6">
{loadingDashboard ? (
<div className="flex justify-center py-8"><Loader2 className="h-8 w-8 animate-spin" /></div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Amostras</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-blue-600">{dashboard?.samples?.total || 0}</div>
<p className="text-xs text-muted-foreground">Total cadastradas</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">RNC Abertas</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-red-600">{dashboard?.nonConformities?.open || 0}</div>
<p className="text-xs text-muted-foreground">Pendentes tratamento</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Documentos Vigentes</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-green-600">{dashboard?.documents?.active || 0}</div>
<p className="text-xs text-muted-foreground">QMS ativos</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Treinamentos Vencendo</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-orange-600">{dashboard?.trainings?.expiringSoon || 0}</div>
<p className="text-xs text-muted-foreground">Próximos 30 dias</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Despesas Pendentes</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-yellow-600">{dashboard?.expenses?.pending || 0}</div>
<p className="text-xs text-muted-foreground">Aguardando aprovação</p>
</CardContent>
</Card>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle className="text-lg">Últimas Amostras</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Código</TableHead>
<TableHead>Tipo</TableHead>
<TableHead>Local</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{samples.slice(0, 5).map((sample) => (
<TableRow key={sample.id}>
<TableCell className="font-medium">{sample.sampleCode}</TableCell>
<TableCell>{sample.sampleType}</TableCell>
<TableCell className="max-w-[150px] truncate">{sample.collectionLocation}</TableCell>
<TableCell>{getStatusBadge(sample.status)}</TableCell>
</TableRow>
))}
{samples.length === 0 && (
<TableRow>
<TableCell colSpan={4} className="text-center text-muted-foreground">
Nenhuma amostra cadastrada
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-lg">RNC Abertas</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Número</TableHead>
<TableHead>Título</TableHead>
<TableHead>Severidade</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{nonConformities.filter(r => r.status === "aberta").slice(0, 5).map((rnc) => (
<TableRow key={rnc.id}>
<TableCell className="font-medium">{rnc.rncNumber}</TableCell>
<TableCell className="max-w-[150px] truncate">{rnc.title}</TableCell>
<TableCell>
<Badge variant={rnc.severity === "critica" ? "destructive" : rnc.severity === "alta" ? "destructive" : "secondary"}>
{rnc.severity}
</Badge>
</TableCell>
<TableCell>{getStatusBadge(rnc.status)}</TableCell>
</TableRow>
))}
{nonConformities.filter(r => r.status === "aberta").length === 0 && (
<TableRow>
<TableCell colSpan={4} className="text-center text-muted-foreground">
Nenhuma RNC aberta
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="samples" className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Search className="h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar amostras..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-64"
data-testid="search-samples"
/>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={() => refetchSamples()}>
<RefreshCw className="h-4 w-4 mr-2" /> Atualizar
</Button>
<Button onClick={() => { setEditingItem(null); setShowSampleDialog(true); }} data-testid="btn-new-sample">
<Plus className="h-4 w-4 mr-2" /> Nova Amostra
</Button>
</div>
</div>
<Card>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Código</TableHead>
<TableHead>Tipo</TableHead>
<TableHead>Data Coleta</TableHead>
<TableHead>Local</TableHead>
<TableHead>Responsável</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredSamples.map((sample) => (
<TableRow key={sample.id}>
<TableCell className="font-medium">{sample.sampleCode}</TableCell>
<TableCell>{sample.sampleType}</TableCell>
<TableCell>{sample.collectionDate ? new Date(sample.collectionDate).toLocaleDateString("pt-BR") : "-"}</TableCell>
<TableCell className="max-w-[200px] truncate">{sample.collectionLocation}</TableCell>
<TableCell>{sample.collectionResponsible}</TableCell>
<TableCell>{getStatusBadge(sample.status)}</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="icon" onClick={() => { setEditingItem(sample); setShowSampleDialog(true); }}>
<Edit className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
{filteredSamples.length === 0 && (
<TableRow>
<TableCell colSpan={7} className="text-center py-8 text-muted-foreground">
Nenhuma amostra encontrada
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="lab-reports" className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">Laudos Laboratoriais</h2>
<Button data-testid="btn-new-report">
<Plus className="h-4 w-4 mr-2" /> Novo Laudo
</Button>
</div>
<Card>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Número</TableHead>
<TableHead>Amostra</TableHead>
<TableHead>Laboratório</TableHead>
<TableHead>Data Emissão</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{labReports.map((report) => (
<TableRow key={report.id}>
<TableCell className="font-medium">{report.reportNumber}</TableCell>
<TableCell>#{report.sampleId}</TableCell>
<TableCell>Lab {report.laboratoryId}</TableCell>
<TableCell>{report.issueDate ? new Date(report.issueDate).toLocaleDateString("pt-BR") : "-"}</TableCell>
<TableCell>{getStatusBadge(report.status)}</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="icon"><Eye className="h-4 w-4" /></Button>
</TableCell>
</TableRow>
))}
{labReports.length === 0 && (
<TableRow>
<TableCell colSpan={6} className="text-center py-8 text-muted-foreground">
Nenhum laudo cadastrado
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="rnc" className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Search className="h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar RNC..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-64"
/>
</div>
<Button onClick={() => { setEditingItem(null); setShowRncDialog(true); }} data-testid="btn-new-rnc">
<Plus className="h-4 w-4 mr-2" /> Nova RNC
</Button>
</div>
<Card>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Número</TableHead>
<TableHead>Título</TableHead>
<TableHead>Tipo</TableHead>
<TableHead>Severidade</TableHead>
<TableHead>Detectado em</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredRnc.map((rnc) => (
<TableRow key={rnc.id}>
<TableCell className="font-medium">{rnc.rncNumber}</TableCell>
<TableCell className="max-w-[200px] truncate">{rnc.title}</TableCell>
<TableCell>
<Badge variant="outline">{rnc.type?.replace(/_/g, " ")}</Badge>
</TableCell>
<TableCell>
<Badge variant={rnc.severity === "critica" || rnc.severity === "alta" ? "destructive" : "secondary"}>
{rnc.severity}
</Badge>
</TableCell>
<TableCell>{rnc.detectedAt ? new Date(rnc.detectedAt).toLocaleDateString("pt-BR") : "-"}</TableCell>
<TableCell>{getStatusBadge(rnc.status)}</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="icon"><Edit className="h-4 w-4" /></Button>
</TableCell>
</TableRow>
))}
{filteredRnc.length === 0 && (
<TableRow>
<TableCell colSpan={7} className="text-center py-8 text-muted-foreground">
Nenhuma não conformidade encontrada
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="documents" className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Search className="h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar documentos..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-64"
/>
</div>
<Button onClick={() => { setEditingItem(null); setShowDocDialog(true); }} data-testid="btn-new-doc">
<Plus className="h-4 w-4 mr-2" /> Novo Documento
</Button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
<div className="lg:col-span-2">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<FileText className="h-4 w-4" />
Documentos do Sistema de Gestão
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Código</TableHead>
<TableHead>Título</TableHead>
<TableHead>Tipo</TableHead>
<TableHead>Versão</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredDocs.map((doc) => (
<TableRow key={doc.id} className="cursor-pointer hover:bg-muted/50">
<TableCell className="font-medium">{doc.documentCode}</TableCell>
<TableCell className="max-w-[200px] truncate">{doc.title}</TableCell>
<TableCell>{doc.type}</TableCell>
<TableCell>{doc.version}</TableCell>
<TableCell>{getStatusBadge(doc.status)}</TableCell>
<TableCell className="text-right">
<Button
variant="ghost"
size="icon"
onClick={() => { setSelectedDoc(doc); setShowDocViewDialog(true); }}
data-testid={`btn-read-doc-${doc.id}`}
>
<Eye className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => { setEditingItem(doc); setShowDocDialog(true); }}
data-testid={`btn-edit-doc-${doc.id}`}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => {
if (confirm("Tem certeza que deseja excluir este documento?")) {
toast({ title: "Documento excluído", description: `${doc.documentCode} foi removido.` });
}
}}
data-testid={`btn-delete-doc-${doc.id}`}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</TableCell>
</TableRow>
))}
{filteredDocs.length === 0 && (
<TableRow>
<TableCell colSpan={6} className="text-center py-8 text-muted-foreground">
Nenhum documento encontrado
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
<div className="space-y-4">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<ClipboardList className="h-4 w-4 text-blue-600" />
Caixa de Leitura
</CardTitle>
<CardDescription>Documentos para leitura obrigatória</CardDescription>
</CardHeader>
<CardContent className="space-y-2">
<div className="p-3 border rounded-lg hover:bg-muted/50 cursor-pointer" data-testid="reading-doc-1">
<div className="flex items-center justify-between">
<span className="font-medium text-sm">POP-001</span>
<Badge variant="outline" className="text-xs">Novo</Badge>
</div>
<p className="text-xs text-muted-foreground mt-1">Procedimento de Amostragem de Água</p>
</div>
<div className="p-3 border rounded-lg hover:bg-muted/50 cursor-pointer" data-testid="reading-doc-2">
<div className="flex items-center justify-between">
<span className="font-medium text-sm">IT-015</span>
<Badge variant="outline" className="text-xs">Atualizado</Badge>
</div>
<p className="text-xs text-muted-foreground mt-1">Instrução Técnica - Monitoramento</p>
</div>
<div className="p-3 border rounded-lg hover:bg-muted/50 cursor-pointer" data-testid="reading-doc-3">
<div className="flex items-center justify-between">
<span className="font-medium text-sm">FT-057</span>
<Badge variant="secondary" className="text-xs">Pendente</Badge>
</div>
<p className="text-xs text-muted-foreground mt-1">Formulário de Treinamento</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base">Categorias de Documentos</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<div className="flex items-center justify-between p-2 hover:bg-muted rounded cursor-pointer">
<span className="text-sm">Procedimentos (POP)</span>
<Badge variant="secondary">12</Badge>
</div>
<div className="flex items-center justify-between p-2 hover:bg-muted rounded cursor-pointer">
<span className="text-sm">Instruções Técnicas (IT)</span>
<Badge variant="secondary">8</Badge>
</div>
<div className="flex items-center justify-between p-2 hover:bg-muted rounded cursor-pointer">
<span className="text-sm">Formulários (FT)</span>
<Badge variant="secondary">25</Badge>
</div>
<div className="flex items-center justify-between p-2 hover:bg-muted rounded cursor-pointer">
<span className="text-sm">Manuais (MQ)</span>
<Badge variant="secondary">3</Badge>
</div>
<div className="flex items-center justify-between p-2 hover:bg-muted rounded cursor-pointer">
<span className="text-sm">Políticas (PL)</span>
<Badge variant="secondary">5</Badge>
</div>
</CardContent>
</Card>
</div>
</div>
</TabsContent>
<TabsContent value="field-forms" className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">Formulários de Campo</h2>
<Button data-testid="btn-new-field-form">
<Plus className="h-4 w-4 mr-2" /> Novo Formulário
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card className="cursor-pointer hover:shadow-md transition-shadow">
<CardHeader>
<CardTitle className="text-base">Plano de Amostragem</CardTitle>
<CardDescription>PA - Planejamento de coleta de amostras</CardDescription>
</CardHeader>
</Card>
<Card className="cursor-pointer hover:shadow-md transition-shadow">
<CardHeader>
<CardTitle className="text-base">Plano de Trabalho (PT)</CardTitle>
<CardDescription>Orientações para equipe de campo</CardDescription>
</CardHeader>
</Card>
<Card className="cursor-pointer hover:shadow-md transition-shadow">
<CardHeader>
<CardTitle className="text-base">Monitoramento de Poços</CardTitle>
<CardDescription>Ficha de medição e coleta</CardDescription>
</CardHeader>
</Card>
<Card className="cursor-pointer hover:shadow-md transition-shadow">
<CardHeader>
<CardTitle className="text-base">Ficha de Campo</CardTitle>
<CardDescription>Registro geral de atividades</CardDescription>
</CardHeader>
</Card>
<Card className="cursor-pointer hover:shadow-md transition-shadow">
<CardHeader>
<CardTitle className="text-base">Checklist Equipamentos</CardTitle>
<CardDescription>Verificação de equipamentos</CardDescription>
</CardHeader>
</Card>
<Card className="cursor-pointer hover:shadow-md transition-shadow">
<CardHeader>
<CardTitle className="text-base">Relatório Fotográfico</CardTitle>
<CardDescription>Documentação visual de campo</CardDescription>
</CardHeader>
</Card>
</div>
<Card>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Código</TableHead>
<TableHead>Tipo</TableHead>
<TableHead>Título</TableHead>
<TableHead>Data</TableHead>
<TableHead>Local</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{fieldForms.map((form) => (
<TableRow key={form.id}>
<TableCell className="font-medium">{form.formCode}</TableCell>
<TableCell>{form.formType}</TableCell>
<TableCell>{form.title}</TableCell>
<TableCell>{form.collectionDate ? new Date(form.collectionDate).toLocaleDateString("pt-BR") : "-"}</TableCell>
<TableCell className="max-w-[150px] truncate">{form.location}</TableCell>
<TableCell>{getStatusBadge(form.status)}</TableCell>
</TableRow>
))}
{fieldForms.length === 0 && (
<TableRow>
<TableCell colSpan={6} className="text-center py-8 text-muted-foreground">
Nenhum formulário de campo cadastrado
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="training" className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold">Matriz de Treinamentos (FT-57)</h2>
<p className="text-sm text-muted-foreground">Controle de competência técnica conforme ISO 17025</p>
</div>
<div className="flex gap-2">
<Button variant="outline" data-testid="btn-training-calendar">
<Calendar className="h-4 w-4 mr-2" /> Agenda
</Button>
<Button onClick={() => { setEditingTraining(null); setShowTrainingDialog(true); }} data-testid="btn-new-training">
<Plus className="h-4 w-4 mr-2" /> Novo Treinamento
</Button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-100 rounded-lg">
<Users className="h-5 w-5 text-blue-600" />
</div>
<div>
<p className="text-2xl font-bold">12</p>
<p className="text-xs text-muted-foreground">Colaboradores</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-3">
<div className="p-2 bg-green-100 rounded-lg">
<Award className="h-5 w-5 text-green-600" />
</div>
<div>
<p className="text-2xl font-bold">45</p>
<p className="text-xs text-muted-foreground">Treinamentos Válidos</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-3">
<div className="p-2 bg-yellow-100 rounded-lg">
<Clock className="h-5 w-5 text-yellow-600" />
</div>
<div>
<p className="text-2xl font-bold">8</p>
<p className="text-xs text-muted-foreground">Vencendo em 30 dias</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-3">
<div className="p-2 bg-red-100 rounded-lg">
<AlertTriangle className="h-5 w-5 text-red-600" />
</div>
<div>
<p className="text-2xl font-bold">3</p>
<p className="text-xs text-muted-foreground">Vencidos</p>
</div>
</div>
</CardContent>
</Card>
</div>
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-base">Registro de Treinamentos</CardTitle>
<div className="flex items-center gap-2">
<Search className="h-4 w-4 text-muted-foreground" />
<Input placeholder="Buscar colaborador ou treinamento..." className="w-64" />
</div>
</div>
</CardHeader>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Colaborador</TableHead>
<TableHead>Treinamento</TableHead>
<TableHead>Tipo</TableHead>
<TableHead>Data Conclusão</TableHead>
<TableHead>Validade</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{(trainings.length === 0 ? sampleTrainings : trainings).map((t: any) => (
<TableRow key={t.id}>
<TableCell className="font-medium">{t.employeeName || t.employeeId}</TableCell>
<TableCell>{t.trainingName}</TableCell>
<TableCell><Badge variant="outline">{t.trainingType}</Badge></TableCell>
<TableCell>{t.completedDate ? new Date(t.completedDate).toLocaleDateString("pt-BR") : "-"}</TableCell>
<TableCell>{t.expiryDate ? new Date(t.expiryDate).toLocaleDateString("pt-BR") : "-"}</TableCell>
<TableCell>
{t.status === "valid" && <Badge className="bg-green-500">Válido</Badge>}
{t.status === "expiring" && <Badge className="bg-yellow-500">Vencendo</Badge>}
{t.status === "expired" && <Badge className="bg-red-500">Vencido</Badge>}
{t.status === "scheduled" && <Badge className="bg-blue-500">Agendado</Badge>}
</TableCell>
<TableCell className="text-right">
<Button
variant="ghost"
size="icon"
onClick={() => { setSelectedTraining(t); setShowTrainingViewDialog(true); }}
data-testid={`btn-view-training-${t.id}`}
>
<Eye className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => { setEditingTraining(t); setShowTrainingDialog(true); }}
data-testid={`btn-edit-training-${t.id}`}
>
<Edit className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="suppliers" className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">Fornecedores Homologados - ISO 17025</h2>
<Button data-testid="btn-homologate">
<Plus className="h-4 w-4 mr-2" /> Homologar Fornecedor
</Button>
</div>
<Card>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Código</TableHead>
<TableHead>Nome</TableHead>
<TableHead>Certificações</TableHead>
<TableHead>Score</TableHead>
<TableHead>Validade</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{homologatedSuppliers.map((s) => (
<TableRow key={s.id}>
<TableCell className="font-medium">{s.code}</TableCell>
<TableCell>{s.name}</TableCell>
<TableCell>
<div className="flex gap-1 flex-wrap">
{s.certifications?.map((c, i) => (
<Badge key={i} variant="outline" className="text-xs">{c}</Badge>
))}
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<div className="w-16 h-2 bg-gray-200 rounded-full overflow-hidden">
<div
className={`h-full ${s.qualityScore >= 80 ? 'bg-green-500' : s.qualityScore >= 60 ? 'bg-yellow-500' : 'bg-red-500'}`}
style={{ width: `${s.qualityScore}%` }}
/>
</div>
<span className="text-sm">{s.qualityScore}%</span>
</div>
</TableCell>
<TableCell>{s.homologationExpiry ? new Date(s.homologationExpiry).toLocaleDateString("pt-BR") : "-"}</TableCell>
<TableCell>{getStatusBadge(s.homologationStatus)}</TableCell>
</TableRow>
))}
{homologatedSuppliers.length === 0 && (
<TableRow>
<TableCell colSpan={6} className="text-center py-8 text-muted-foreground">
Nenhum fornecedor homologado
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
</Tabs>
<Dialog open={showSampleDialog} onOpenChange={setShowSampleDialog}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>{editingItem ? "Editar Amostra" : "Nova Amostra"}</DialogTitle>
</DialogHeader>
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
createSampleMutation.mutate({
sampleCode: formData.get("sampleCode") as string,
sampleType: formData.get("sampleType") as string,
collectionLocation: formData.get("collectionLocation") as string,
collectionResponsible: formData.get("collectionResponsible") as string,
status: "coletada",
});
}}>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="sampleCode">Código da Amostra *</Label>
<Input id="sampleCode" name="sampleCode" placeholder="AM-2026-001" required />
</div>
<div className="space-y-2">
<Label htmlFor="sampleType">Tipo</Label>
<Select name="sampleType" defaultValue="agua">
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="agua">Água</SelectItem>
<SelectItem value="solo">Solo</SelectItem>
<SelectItem value="sedimento">Sedimento</SelectItem>
<SelectItem value="efluente">Efluente</SelectItem>
<SelectItem value="ar">Ar</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="collectionLocation">Local de Coleta</Label>
<Input id="collectionLocation" name="collectionLocation" placeholder="Ponto A1 - Poço PM-01" />
</div>
<div className="space-y-2">
<Label htmlFor="collectionResponsible">Responsável pela Coleta</Label>
<Input id="collectionResponsible" name="collectionResponsible" placeholder="Nome do técnico" />
</div>
</div>
<DialogFooter className="mt-6">
<Button type="button" variant="outline" onClick={() => setShowSampleDialog(false)}>Cancelar</Button>
<Button type="submit" disabled={createSampleMutation.isPending}>
{createSampleMutation.isPending ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : null}
Salvar
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<Dialog open={showRncDialog} onOpenChange={setShowRncDialog}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>Nova Não Conformidade</DialogTitle>
</DialogHeader>
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
createRncMutation.mutate({
rncNumber: `RNC-${Date.now()}`,
title: formData.get("title") as string,
description: formData.get("description") as string,
type: formData.get("type") as string,
severity: formData.get("severity") as string,
detectedBy: formData.get("detectedBy") as string,
status: "aberta",
});
}}>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="title">Título *</Label>
<Input id="title" name="title" placeholder="Descrição resumida da NC" required />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="type">Tipo</Label>
<Select name="type" defaultValue="nao_conformidade">
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="nao_conformidade">Não Conformidade</SelectItem>
<SelectItem value="acao_corretiva">Ação Corretiva</SelectItem>
<SelectItem value="oportunidade_melhoria">Oportunidade de Melhoria</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="severity">Severidade</Label>
<Select name="severity" defaultValue="media">
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="baixa">Baixa</SelectItem>
<SelectItem value="media">Média</SelectItem>
<SelectItem value="alta">Alta</SelectItem>
<SelectItem value="critica">Crítica</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="description">Descrição</Label>
<Textarea id="description" name="description" placeholder="Descreva a não conformidade em detalhes..." />
</div>
<div className="space-y-2">
<Label htmlFor="detectedBy">Detectado por</Label>
<Input id="detectedBy" name="detectedBy" placeholder="Nome de quem identificou" />
</div>
</div>
<DialogFooter className="mt-6">
<Button type="button" variant="outline" onClick={() => setShowRncDialog(false)}>Cancelar</Button>
<Button type="submit" disabled={createRncMutation.isPending}>
{createRncMutation.isPending ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : null}
Criar RNC
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<Dialog open={showDocDialog} onOpenChange={(open) => { setShowDocDialog(open); if (!open) { setEditingItem(null); setSelectedFile(null); } }}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{editingItem ? "Editar Documento" : "Novo Documento QMS"}</DialogTitle>
</DialogHeader>
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
if (editingItem) {
toast({ title: "Documento atualizado", description: `${formData.get("documentCode")} foi atualizado com sucesso.` });
} else {
createDocMutation.mutate({
documentCode: formData.get("documentCode") as string,
title: formData.get("title") as string,
type: formData.get("type") as string,
category: formData.get("category") as string,
version: "01",
status: "vigente",
});
}
setShowDocDialog(false);
setEditingItem(null);
}}>
<div className="space-y-4">
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="documentCode">Código *</Label>
<Input
id="documentCode"
name="documentCode"
placeholder="FT-01, PG-01, IT-01"
defaultValue={editingItem?.documentCode || ""}
required
data-testid="input-doc-code"
/>
</div>
<div className="space-y-2">
<Label htmlFor="type">Tipo</Label>
<Select name="type" defaultValue={editingItem?.type || "formulario"}>
<SelectTrigger data-testid="select-doc-type"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="formulario">Formulário (FT)</SelectItem>
<SelectItem value="procedimento">Procedimento (POP)</SelectItem>
<SelectItem value="instrucao">Instrução Técnica (IT)</SelectItem>
<SelectItem value="manual">Manual (MQ)</SelectItem>
<SelectItem value="politica">Política (PL)</SelectItem>
<SelectItem value="registro">Registro (RG)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="version">Versão</Label>
<Input
id="version"
name="version"
placeholder="01"
defaultValue={editingItem?.version || "01"}
data-testid="input-doc-version"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="title">Título *</Label>
<Input
id="title"
name="title"
placeholder="Nome do documento"
defaultValue={editingItem?.title || ""}
required
data-testid="input-doc-title"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="category">Categoria</Label>
<Select name="category" defaultValue={editingItem?.category || "qualidade"}>
<SelectTrigger data-testid="select-doc-category"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="qualidade">Qualidade</SelectItem>
<SelectItem value="operacional">Operacional</SelectItem>
<SelectItem value="tecnico">Técnico</SelectItem>
<SelectItem value="administrativo">Administrativo</SelectItem>
<SelectItem value="seguranca">Segurança</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="status">Status</Label>
<Select name="status" defaultValue={editingItem?.status || "vigente"}>
<SelectTrigger data-testid="select-doc-status"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="rascunho">Rascunho</SelectItem>
<SelectItem value="em_revisao">Em Revisão</SelectItem>
<SelectItem value="vigente">Vigente</SelectItem>
<SelectItem value="obsoleto">Obsoleto</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label>Anexar Arquivo</Label>
{selectedFile ? (
<div className="border rounded-lg p-4 bg-muted/30">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-primary/10 rounded">
<FileText className="h-6 w-6 text-primary" />
</div>
<div>
<p className="font-medium text-sm">{selectedFile.name}</p>
<p className="text-xs text-muted-foreground">
{(selectedFile.size / 1024).toFixed(1)} KB
</p>
</div>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setSelectedFile(null)}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
</div>
) : (
<div
className="border-2 border-dashed rounded-lg p-6 text-center hover:bg-muted/50 cursor-pointer transition-colors"
onClick={() => document.getElementById('doc-file-input')?.click()}
>
<FileUp className="h-8 w-8 mx-auto text-muted-foreground mb-2" />
<p className="text-sm text-muted-foreground">
Arraste um arquivo ou clique para selecionar
</p>
<p className="text-xs text-muted-foreground mt-1">
PDF, DOC, DOCX, XLS, XLSX (máx. 10MB)
</p>
<Button
type="button"
variant="outline"
size="sm"
className="mt-3"
onClick={(e) => { e.stopPropagation(); document.getElementById('doc-file-input')?.click(); }}
>
<Upload className="h-4 w-4 mr-2" />
Selecionar Arquivo
</Button>
</div>
)}
<input
id="doc-file-input"
type="file"
className="hidden"
accept=".pdf,.doc,.docx,.xls,.xlsx"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
setSelectedFile(file);
toast({ title: "Arquivo selecionado", description: file.name });
}
}}
data-testid="input-doc-file"
/>
</div>
<div className="space-y-2">
<Label htmlFor="observations">Observações</Label>
<Textarea
id="observations"
name="observations"
placeholder="Observações sobre o documento..."
rows={3}
data-testid="input-doc-observations"
/>
</div>
</div>
<DialogFooter className="mt-6">
<Button type="button" variant="outline" onClick={() => { setShowDocDialog(false); setEditingItem(null); }}>
Cancelar
</Button>
<Button type="submit" disabled={createDocMutation.isPending} data-testid="btn-save-doc">
{createDocMutation.isPending ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : null}
{editingItem ? "Salvar Alterações" : "Criar Documento"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<Dialog open={showDocViewDialog} onOpenChange={setShowDocViewDialog}>
<DialogContent className="max-w-4xl max-h-[80vh]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<BookOpen className="h-5 w-5" />
{selectedDoc?.documentCode} - {selectedDoc?.title}
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-4 gap-4 text-sm">
<div>
<span className="text-muted-foreground">Tipo:</span>
<p className="font-medium">{selectedDoc?.type}</p>
</div>
<div>
<span className="text-muted-foreground">Categoria:</span>
<p className="font-medium">{selectedDoc?.category}</p>
</div>
<div>
<span className="text-muted-foreground">Versão:</span>
<p className="font-medium">{selectedDoc?.version}</p>
</div>
<div>
<span className="text-muted-foreground">Status:</span>
<p className="font-medium">{selectedDoc?.status}</p>
</div>
</div>
{viewingFileUrl ? (
<div className="border rounded-lg overflow-hidden min-h-[500px]">
<iframe
src={viewingFileUrl}
className="w-full h-[500px]"
title="Visualizador de documento"
/>
</div>
) : (
<div className="border rounded-lg p-8 min-h-[400px] bg-muted/30 flex flex-col items-center justify-center">
<FileText className="h-16 w-16 text-muted-foreground mb-4" />
<p className="text-muted-foreground text-center">
Visualizador de documento
</p>
<p className="text-sm text-muted-foreground text-center mt-1">
Selecione um documento da Caixa de Leitura ou anexe um arquivo para visualizar.
</p>
<div className="flex gap-2 mt-4">
<Button
variant="outline"
onClick={() => document.getElementById('view-file-input')?.click()}
data-testid="btn-open-local-doc"
>
<Upload className="h-4 w-4 mr-2" />
Abrir Arquivo Local
</Button>
</div>
<input
id="view-file-input"
type="file"
className="hidden"
accept=".pdf"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
const url = URL.createObjectURL(file);
setViewingFileUrl(url);
}
}}
/>
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => { setShowDocViewDialog(false); setViewingFileUrl(null); }}>
Fechar
</Button>
<Button onClick={() => { setShowDocViewDialog(false); setEditingItem(selectedDoc); setShowDocDialog(true); }} data-testid="btn-edit-from-view">
<Edit className="h-4 w-4 mr-2" />
Editar Documento
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={showTrainingDialog} onOpenChange={(open) => { setShowTrainingDialog(open); if (!open) setEditingTraining(null); }}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<GraduationCap className="h-5 w-5" />
{editingTraining ? "Editar Treinamento" : "Novo Treinamento"}
</DialogTitle>
<DialogDescription>
Registre treinamentos e competências dos colaboradores conforme FT-57
</DialogDescription>
</DialogHeader>
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
toast({ title: "Treinamento salvo", description: "Registro salvo com sucesso." });
setShowTrainingDialog(false);
}}>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="employee">Colaborador *</Label>
<Select name="employee" defaultValue={editingTraining?.employeeId}>
<SelectTrigger data-testid="select-training-employee">
<SelectValue placeholder="Selecionar colaborador" />
</SelectTrigger>
<SelectContent>
<SelectItem value="emp-001">João Silva - Técnico Ambiental</SelectItem>
<SelectItem value="emp-002">Maria Santos - Eng. Ambiental</SelectItem>
<SelectItem value="emp-003">Carlos Oliveira - Operador Sonda</SelectItem>
<SelectItem value="emp-004">Ana Costa - Coord. Qualidade</SelectItem>
<SelectItem value="emp-005">Pedro Lima - Técnico Laboratório</SelectItem>
<SelectItem value="emp-006">Fernanda Souza - Geóloga</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">Sincronizado com HRM</p>
</div>
<div className="space-y-2">
<Label htmlFor="trainingType">Tipo de Treinamento *</Label>
<Select name="trainingType" defaultValue={editingTraining?.trainingType}>
<SelectTrigger data-testid="select-training-type">
<SelectValue placeholder="Selecionar tipo" />
</SelectTrigger>
<SelectContent>
<SelectItem value="tecnico">Técnico</SelectItem>
<SelectItem value="seguranca">Segurança</SelectItem>
<SelectItem value="qualidade">Qualidade</SelectItem>
<SelectItem value="normativo">Normativo</SelectItem>
<SelectItem value="operacional">Operacional</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="trainingName">Nome do Treinamento *</Label>
<Input
id="trainingName"
name="trainingName"
defaultValue={editingTraining?.trainingName}
placeholder="Ex: Amostragem de Água Subterrânea"
required
data-testid="input-training-name"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="completedDate">Data de Conclusão</Label>
<Input
id="completedDate"
name="completedDate"
type="date"
defaultValue={editingTraining?.completedDate?.split('T')[0]}
data-testid="input-training-completed"
/>
</div>
<div className="space-y-2">
<Label htmlFor="expiryDate">Data de Validade</Label>
<Input
id="expiryDate"
name="expiryDate"
type="date"
defaultValue={editingTraining?.expiryDate?.split('T')[0]}
data-testid="input-training-expiry"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="scheduledDate">Data Agendada (Futuro)</Label>
<Input
id="scheduledDate"
name="scheduledDate"
type="date"
data-testid="input-training-scheduled"
/>
<p className="text-xs text-muted-foreground">Para treinamentos ainda não realizados</p>
</div>
<div className="space-y-2">
<Label htmlFor="instructor">Instrutor/Entidade</Label>
<Input
id="instructor"
name="instructor"
placeholder="Nome do instrutor ou entidade"
data-testid="input-training-instructor"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="workload">Carga Horária (h)</Label>
<Input
id="workload"
name="workload"
type="number"
placeholder="8"
data-testid="input-training-workload"
/>
</div>
<div className="space-y-2">
<Label htmlFor="status">Status</Label>
<Select name="status" defaultValue={editingTraining?.status || "scheduled"}>
<SelectTrigger data-testid="select-training-status">
<SelectValue placeholder="Selecionar status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="scheduled">Agendado</SelectItem>
<SelectItem value="valid">Válido</SelectItem>
<SelectItem value="expiring">Vencendo</SelectItem>
<SelectItem value="expired">Vencido</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label>Certificado/Evidência</Label>
<div
className="border-2 border-dashed rounded-lg p-4 text-center cursor-pointer hover:bg-muted/50 transition-colors"
onClick={() => document.getElementById('training-file-input')?.click()}
>
<Upload className="h-6 w-6 mx-auto mb-2 text-muted-foreground" />
<p className="text-sm text-muted-foreground">Clique para anexar certificado</p>
<p className="text-xs text-muted-foreground mt-1">PDF, JPG, PNG (máx. 5MB)</p>
</div>
<input
id="training-file-input"
type="file"
className="hidden"
accept=".pdf,.jpg,.jpeg,.png"
data-testid="input-training-file"
/>
</div>
<div className="space-y-2">
<Label htmlFor="observations">Observações</Label>
<Textarea
id="observations"
name="observations"
placeholder="Observações adicionais sobre o treinamento..."
rows={2}
data-testid="input-training-observations"
/>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setShowTrainingDialog(false)}>
Cancelar
</Button>
<Button type="submit" data-testid="btn-save-training">
{editingTraining ? "Salvar Alterações" : "Registrar Treinamento"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<Dialog open={showTrainingViewDialog} onOpenChange={setShowTrainingViewDialog}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<GraduationCap className="h-5 w-5" />
Detalhes do Treinamento
</DialogTitle>
</DialogHeader>
{selectedTraining && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<span className="text-sm text-muted-foreground">Colaborador</span>
<p className="font-medium">{selectedTraining.employeeName || selectedTraining.employeeId}</p>
</div>
<div>
<span className="text-sm text-muted-foreground">Tipo</span>
<p className="font-medium">{selectedTraining.trainingType}</p>
</div>
</div>
<div>
<span className="text-sm text-muted-foreground">Treinamento</span>
<p className="font-medium text-lg">{selectedTraining.trainingName}</p>
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<span className="text-sm text-muted-foreground">Data Conclusão</span>
<p className="font-medium">{selectedTraining.completedDate ? new Date(selectedTraining.completedDate).toLocaleDateString("pt-BR") : "-"}</p>
</div>
<div>
<span className="text-sm text-muted-foreground">Validade</span>
<p className="font-medium">{selectedTraining.expiryDate ? new Date(selectedTraining.expiryDate).toLocaleDateString("pt-BR") : "-"}</p>
</div>
<div>
<span className="text-sm text-muted-foreground">Status</span>
<div className="mt-1">
{selectedTraining.status === "valid" && <Badge className="bg-green-500">Válido</Badge>}
{selectedTraining.status === "expiring" && <Badge className="bg-yellow-500">Vencendo</Badge>}
{selectedTraining.status === "expired" && <Badge className="bg-red-500">Vencido</Badge>}
{selectedTraining.status === "scheduled" && <Badge className="bg-blue-500">Agendado</Badge>}
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<span className="text-sm text-muted-foreground">Instrutor/Entidade</span>
<p className="font-medium">{selectedTraining.instructor || "-"}</p>
</div>
<div>
<span className="text-sm text-muted-foreground">Carga Horária</span>
<p className="font-medium">{selectedTraining.workload ? `${selectedTraining.workload}h` : "-"}</p>
</div>
</div>
<div className="border-t pt-4 flex justify-end gap-2">
<Button variant="outline" onClick={() => setShowTrainingViewDialog(false)}>
Fechar
</Button>
<Button onClick={() => { setShowTrainingViewDialog(false); setEditingTraining(selectedTraining); setShowTrainingDialog(true); }}>
<Edit className="h-4 w-4 mr-2" />
Editar
</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
</div>
</BrowserFrame>
);
}