From ad9b9facbfa648e51e96e818abf599f1b20ad342 Mon Sep 17 00:00:00 2001 From: Wanderson Soares Date: Tue, 16 Jun 2026 06:54:46 -0300 Subject: [PATCH 1/7] refactor: isolate validation mocks and update api --- frontend/src/lib/api/validation.ts | 118 +++++++++++++++++++++++ frontend/src/lib/mock-validation-data.ts | 41 ++++++++ 2 files changed, 159 insertions(+) create mode 100644 frontend/src/lib/api/validation.ts create mode 100644 frontend/src/lib/mock-validation-data.ts diff --git a/frontend/src/lib/api/validation.ts b/frontend/src/lib/api/validation.ts new file mode 100644 index 0000000..589532b --- /dev/null +++ b/frontend/src/lib/api/validation.ts @@ -0,0 +1,118 @@ +import { type PaginatedResponse } from './client'; +import { asRecord, asString } from './normalizers'; + +import { mockValidationCandidates } from '@/lib/mock-validation-data'; + +// ── Types ──────────────────────────────────────────────────────────── + +export type ValidationStatus = 'pending' | 'in_progress' | 'completed'; + +export interface CandidateValidationSummary { + enrollmentId: string; + candidateName: string; + candidateEmail: string; + themeName: string; + professorName?: string; + level?: string; + declaredScore: number; + validatedScore: number | null; + status: ValidationStatus; + submittedAt: string; +} + +export type PaginatedValidationCandidates = PaginatedResponse; + +export interface SecretaryDashboardStats { + total: number; + validated: number; + pending: number; + inProgress: number; +} + +// ── Normalizers ────────────────────────────────────────────────────── + +function normalizeCandidateValidation(data: unknown): CandidateValidationSummary { + const r = asRecord(data); + return { + enrollmentId: asString(r.enrollmentId), + candidateName: asString(r.candidateName), + candidateEmail: asString(r.candidateEmail), + themeName: asString(r.themeName), + professorName: r.professorName ? asString(r.professorName) : undefined, + level: r.level ? asString(r.level) : undefined, + declaredScore: typeof r.declaredScore === 'number' ? r.declaredScore : 0, + validatedScore: typeof r.validatedScore === 'number' ? r.validatedScore : null, + status: asString(r.status) as ValidationStatus, + submittedAt: asString(r.submittedAt), + }; +} + +// ── Endpoints ──────────────────────────────────────────────────────── + +export const validationApi = { + getSecretaryStats: async (): Promise => { + return new Promise(resolve => { + setTimeout(() => { + resolve({ + total: 120, + validated: 45, + pending: 65, + inProgress: 10, + }); + }, 600); + }); + }, + + findCandidates: async (params?: { + page?: number; + limit?: number; + search?: string; + level?: string; + status?: string; + professor?: string; + }): Promise => { + return new Promise(resolve => { + setTimeout(() => { + let mockData = [...mockValidationCandidates]; + + if (params?.search) { + const search = params.search.toLowerCase(); + mockData = mockData.filter(c => c.candidateName.toLowerCase().includes(search)); + } + + if (params?.level && params.level !== 'all') { + const levelFilter = params.level.toLowerCase(); + mockData = mockData.filter(c => c.level?.toLowerCase() === levelFilter); + } + + if (params?.status && params.status !== 'all') { + const statusFilter = params.status; + mockData = mockData.filter(c => c.status === statusFilter); + } + + if (params?.professor && params.professor !== 'all') { + const professorFilter = params.professor.toLowerCase(); + mockData = mockData.filter(c => c.professorName?.toLowerCase().includes(professorFilter)); + } + + resolve({ + data: mockData, + pagination: { + page: params?.page || 1, + limit: params?.limit || 10, + total: mockData.length, + totalPages: 1, + }, + }); + }, 800); + }); + }, + + getDetails: async (enrollmentId: string) => { + return { id: enrollmentId }; + }, + + updateScore: async (enrollmentId: string, itemId: string, score: number | null) => { + return new Promise(resolve => setTimeout(resolve, 500)); + }, +}; diff --git a/frontend/src/lib/mock-validation-data.ts b/frontend/src/lib/mock-validation-data.ts new file mode 100644 index 0000000..e7f83ed --- /dev/null +++ b/frontend/src/lib/mock-validation-data.ts @@ -0,0 +1,41 @@ +// src/lib/mock-validation-data.ts +import type { CandidateValidationSummary } from '@/lib/api/validation'; + +export const mockValidationCandidates: CandidateValidationSummary[] = [ + { + enrollmentId: 'enr-001', + candidateName: 'Antônio Gabriel', + candidateEmail: 'agabriel@email.com', + themeName: 'Observabilidade em Engenharia de Software', + professorName: 'Dr. Lincoln', + level: 'Mestrado', + declaredScore: 45.5, + validatedScore: null, + status: 'pending', + submittedAt: '2026-06-10T14:00:00Z', + }, + { + enrollmentId: 'enr-002', + candidateName: 'Said Costa', + candidateEmail: 'said.costa@email.com', + themeName: 'Machine Learning Aplicado', + professorName: 'Dra. Mariana', + level: 'Doutorado', + declaredScore: 62.0, + validatedScore: 40.5, + status: 'in_progress', + submittedAt: '2026-06-11T09:30:00Z', + }, + { + enrollmentId: 'enr-003', + candidateName: 'Laura Silva', + candidateEmail: 'laura.s@email.com', + themeName: 'Otimização de Redes', + professorName: 'Dr. Lincoln', + level: 'Mestrado', + declaredScore: 30.0, + validatedScore: 30.0, + status: 'completed', + submittedAt: '2026-06-12T10:15:00Z', + }, +]; From b8f4294328856ac22678899254e7ba5407c90286 Mon Sep 17 00:00:00 2001 From: Wanderson Soares Date: Tue, 16 Jun 2026 06:54:58 -0300 Subject: [PATCH 2/7] feat: add secretary hooks and types --- .../validation/hooks/use-validation.ts | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 frontend/src/features/validation/hooks/use-validation.ts diff --git a/frontend/src/features/validation/hooks/use-validation.ts b/frontend/src/features/validation/hooks/use-validation.ts new file mode 100644 index 0000000..374eddb --- /dev/null +++ b/frontend/src/features/validation/hooks/use-validation.ts @@ -0,0 +1,96 @@ +import { queryOptions, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; + +import { validationApi } from '@/lib/api/validation'; + +import type { CandidateValidationSummary, ValidationStatus } from '@/lib/api/validation'; + +export type { CandidateValidationSummary, ValidationStatus }; + +// ── Types ──────────────────────────────────────────────────────────── + +export interface UseValidationCandidatesOptions { + page: number; + limit: number; + search: string; +} + +export interface UseSecretaryCandidatesOptions extends UseValidationCandidatesOptions { + level: string; + status: string; + professor: string; +} + +// ── Query Options ──────────────────────────────────────────────────── + +export function validationCandidatesQueryOptions(options: UseValidationCandidatesOptions) { + return queryOptions({ + queryKey: ['validation', 'candidates', options], + queryFn: () => validationApi.findCandidates(options), + staleTime: 2 * 60 * 1000, + }); +} + +export function secretaryCandidatesQueryOptions(options: UseSecretaryCandidatesOptions) { + return queryOptions({ + queryKey: ['validation', 'secretary', 'candidates', options], + queryFn: () => validationApi.findCandidates(options), + staleTime: 2 * 60 * 1000, + }); +} + +export function secretaryStatsQueryOptions() { + return queryOptions({ + queryKey: ['validation', 'secretary', 'stats'], + queryFn: () => validationApi.getSecretaryStats(), + staleTime: 5 * 60 * 1000, + }); +} + +export function validationDetailsQueryOptions(enrollmentId: string) { + return queryOptions({ + queryKey: ['validation', 'enrollment', enrollmentId], + queryFn: () => validationApi.getDetails(enrollmentId), + enabled: !!enrollmentId, + }); +} + +// ── Hooks ──────────────────────────────────────────────────────────── + +export function useValidationCandidates(options: UseValidationCandidatesOptions) { + return useQuery(validationCandidatesQueryOptions(options)); +} + +export function useSecretaryCandidates(options: UseSecretaryCandidatesOptions) { + return useQuery(secretaryCandidatesQueryOptions(options)); +} + +export function useSecretaryStats() { + return useQuery(secretaryStatsQueryOptions()); +} + +export function useValidationDetails(enrollmentId: string) { + return useQuery(validationDetailsQueryOptions(enrollmentId)); +} + +export function useUpdateValidationScore() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ + enrollmentId, + itemId, + score, + }: { + enrollmentId: string; + itemId: string; + score: number | null; + }) => validationApi.updateScore(enrollmentId, itemId, score), + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ + queryKey: ['validation', 'enrollment', variables.enrollmentId], + }); + queryClient.invalidateQueries({ queryKey: ['validation', 'candidates'] }); + queryClient.invalidateQueries({ queryKey: ['validation', 'secretary'] }); + }, + }); +} From 546a1b3642491842f15a3df5f048c51490ba6c4f Mon Sep 17 00:00:00 2001 From: Wanderson Soares Date: Tue, 16 Jun 2026 06:55:21 -0300 Subject: [PATCH 3/7] feat: implement secretary dashboard and validation table --- frontend/src/components/ui/progress.tsx | 24 ++ .../components/SecretaryDashboard.tsx | 177 ++++++++++++ .../components/SecretaryValidationTable.tsx | 87 ++++++ .../validation/components/ValidationForm.tsx | 257 ++++++++++++++++++ .../components/ValidationHeader.tsx | 19 ++ .../components/ValidationListing.tsx | 54 ++++ .../validation/components/ValidationTable.tsx | 172 ++++++++++++ .../components/ValidationWorkspace.tsx | 143 ++++++++++ 8 files changed, 933 insertions(+) create mode 100644 frontend/src/components/ui/progress.tsx create mode 100644 frontend/src/features/validation/components/SecretaryDashboard.tsx create mode 100644 frontend/src/features/validation/components/SecretaryValidationTable.tsx create mode 100644 frontend/src/features/validation/components/ValidationForm.tsx create mode 100644 frontend/src/features/validation/components/ValidationHeader.tsx create mode 100644 frontend/src/features/validation/components/ValidationListing.tsx create mode 100644 frontend/src/features/validation/components/ValidationTable.tsx create mode 100644 frontend/src/features/validation/components/ValidationWorkspace.tsx diff --git a/frontend/src/components/ui/progress.tsx b/frontend/src/components/ui/progress.tsx new file mode 100644 index 0000000..6ec625d --- /dev/null +++ b/frontend/src/components/ui/progress.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; + +import * as ProgressPrimitive from '@radix-ui/react-progress'; + +import { cn } from '@/lib/utils'; + +const Progress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)); +Progress.displayName = ProgressPrimitive.Root.displayName; + +export { Progress }; diff --git a/frontend/src/features/validation/components/SecretaryDashboard.tsx b/frontend/src/features/validation/components/SecretaryDashboard.tsx new file mode 100644 index 0000000..6f7510b --- /dev/null +++ b/frontend/src/features/validation/components/SecretaryDashboard.tsx @@ -0,0 +1,177 @@ +import { useState } from 'react'; + +import { CheckCircle2, Clock, Filter, Loader2, Search, TrendingUp, Users } from 'lucide-react'; + +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Progress } from '@/components/ui/progress'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; + +import { SecretaryValidationTable } from '@/features/validation/components/SecretaryValidationTable'; + +import { + useSecretaryCandidates, + useSecretaryStats, +} from '@/features/validation/hooks/use-validation'; + +export function SecretaryDashboard() { + const [search, setSearch] = useState(''); + const [level, setLevel] = useState('all'); + const [status, setStatus] = useState('all'); + const [professor, setProfessor] = useState('all'); + const [page, setPage] = useState(1); + + const { data: stats, isLoading: loadingStats } = useSecretaryStats(); + const { data: candidatesData, isLoading: loadingCandidates } = useSecretaryCandidates({ + page, + limit: 10, + search, + level, + status, + professor, + }); + + const total = stats?.total ?? 0; + const validated = stats?.validated ?? 0; + const pending = stats?.pending ?? 0; + const progressPercentage = total > 0 ? Math.round((validated / total) * 100) : 0; + + return ( +
+ {/* Dashboard KPIs */} +
+ + + Total de Candidatos + + + +
+ {loadingStats ? ( + + ) : ( + total + )} +
+

Inscrições submetidas

+
+
+ + + + Validações Concluídas + + + +
+ {loadingStats ? : validated} +
+

Currículos revisados

+
+
+ + + + Aguardando Revisão + + + +
+ {loadingStats ? : pending} +
+

Currículos pendentes

+
+
+ + + + Progresso Geral + + + +
+ {loadingStats ? ( + + ) : ( + `${progressPercentage}%` + )} +
+ +
+
+
+ + {/* Filters */} + + +
+
+ + setSearch(e.target.value)} + /> +
+ +
+
+ + Filtros: +
+ + + + + + +
+
+
+
+ + {/* Candidates Table */} + +
+ ); +} diff --git a/frontend/src/features/validation/components/SecretaryValidationTable.tsx b/frontend/src/features/validation/components/SecretaryValidationTable.tsx new file mode 100644 index 0000000..bc197ef --- /dev/null +++ b/frontend/src/features/validation/components/SecretaryValidationTable.tsx @@ -0,0 +1,87 @@ +import { Link } from '@tanstack/react-router'; + +import { ChevronRight } from 'lucide-react'; + +import type { ColumnDef } from '@tanstack/react-table'; + +import { Table } from '@/components/table'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; + +import { type CandidateValidationSummary } from '@/lib/api/validation'; + +interface SecretaryValidationTableProps { + data: CandidateValidationSummary[]; + loading: boolean; + onPageChange: (page: number) => void; + currentPage: number; +} + +export function SecretaryValidationTable({ + data, + loading, + onPageChange, + currentPage, +}: SecretaryValidationTableProps) { + const columns: ColumnDef[] = [ + { + accessorKey: 'candidateName', + header: 'CANDIDATO', + cell: ({ row }) => ( +
+

{row.original.candidateName}

+

{row.original.candidateEmail}

+
+ ), + }, + { + accessorKey: 'level', + header: 'NÍVEL', + }, + { + accessorKey: 'themeName', + header: 'TEMA', + }, + { + accessorKey: 'professorName', + header: 'PROFESSOR', + }, + { + accessorKey: 'status', + header: 'STATUS', + cell: ({ row }) => ( + + {row.original.status} + + ), + }, + ]; + + const quickActions: ColumnDef[] = [ + { + id: 'actions', + cell: ({ row }) => ( + + ), + }, + ]; + + return ( + {}} + /> + ); +} diff --git a/frontend/src/features/validation/components/ValidationForm.tsx b/frontend/src/features/validation/components/ValidationForm.tsx new file mode 100644 index 0000000..7c45386 --- /dev/null +++ b/frontend/src/features/validation/components/ValidationForm.tsx @@ -0,0 +1,257 @@ +import { useCallback, useMemo, useState } from 'react'; + +import { AlertCircle, Check, CheckCheck, Eye, Loader2 } from 'lucide-react'; +import { toast } from 'sonner'; + +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Skeleton } from '@/components/ui/skeleton'; + +import { useCvItems, useScoringCategories } from '@/features/enrollment/hooks/use-cv-scoring'; +import { useEnrollment } from '@/features/enrollment/hooks/use-enrollment'; +import { useUpdateValidationScore } from '@/features/validation/hooks/use-validation'; +import { api, type CvItem, type ScoringCategory } from '@/lib/api'; + +interface ValidationFormProps { + enrollmentId: string; + onSelectPdf: (url: string) => void; +} + +function computeItemScores(item: CvItem, category: ScoringCategory) { + const declared = item.quantity * parseFloat(category.pointsPerItem); + const validated = item.score !== null ? parseFloat(item.score) : null; + return { declared, validated }; +} + +export function ValidationForm({ enrollmentId, onSelectPdf }: ValidationFormProps) { + const { data: enrollment, isLoading: loadingEnrollment } = useEnrollment(enrollmentId); + const periodId = enrollment?.enrollmentPeriodId ?? ''; + + const level = enrollment?.level ?? ''; + const { data: categories, isLoading: loadingCats } = useScoringCategories(periodId ?? '', level); + const { data: cvItems, isLoading: loadingItems } = useCvItems(enrollmentId); + + const updateScoreMutation = useUpdateValidationScore(); + + const [draftScores, setDraftScores] = useState>({}); + const [savingItems, setSavingItems] = useState>({}); + + const itemsByCategory = useMemo(() => { + const map = new Map(); + for (const item of cvItems ?? []) { + const list = map.get(item.scoringCategoryId) ?? []; + list.push(item); + map.set(item.scoringCategoryId, list); + } + return map; + }, [cvItems]); + + const sortedCategories = useMemo( + () => [...(categories ?? [])].sort((a, b) => a.sortOrder - b.sortOrder), + [categories], + ); + + const handleViewPdf = useCallback( + async (itemId: string) => { + try { + const url = await api.cvItems.getFileUrl(enrollmentId, itemId); + onSelectPdf(url); + } catch { + toast.error('Erro ao carregar o comprovante deste item.'); + } + }, + [enrollmentId, onSelectPdf], + ); + + const handleSaveScore = useCallback( + (itemId: string, scoreValue: number | null) => { + setSavingItems(prev => ({ ...prev, [itemId]: true })); + updateScoreMutation.mutate( + { enrollmentId, itemId, score: scoreValue }, + { + onSuccess: () => { + toast.success('Nota salva com sucesso.'); + setSavingItems(prev => ({ ...prev, [itemId]: false })); + }, + onError: () => { + toast.error('Erro ao salvar a nota.'); + setSavingItems(prev => ({ ...prev, [itemId]: false })); + }, + }, + ); + }, + [enrollmentId, updateScoreMutation], + ); + + const handleBulkAccept = useCallback( + (category: ScoringCategory, items: CvItem[]) => { + items.forEach(item => { + const { declared, validated } = computeItemScores(item, category); + if (validated !== declared) { + handleSaveScore(item.id, declared); + setDraftScores(prev => ({ ...prev, [item.id]: declared.toString() })); + } + }); + toast.success(`Todos os itens de "${category.name}" foram aceitos.`); + }, + [handleSaveScore], + ); + + if (loadingEnrollment || loadingCats || loadingItems) { + return ( +
+ + +
+ ); + } + + if (!sortedCategories.length) { + return

Nenhuma categoria encontrada para este edital.

; + } + + return ( +
+ {sortedCategories.map(category => { + const items = itemsByCategory.get(category.id) ?? []; + if (items.length === 0) return null; + + return ( + + +
+ {category.name} +

+ Máximo: {category.maxPoints} pts | {category.pointsPerItem} pts por item +

+
+ + {/* Opção Bulk Accept */} + +
+ + + {items.map(item => { + const { declared, validated } = computeItemScores(item, category); + const currentDraft = + draftScores[item.id] ?? (validated !== null ? validated.toString() : ''); + const isSaving = savingItems[item.id]; + const isPending = validated === null; + const isDiff = validated !== null && validated !== declared; + + return ( +
+
+ {/* Lado Esquerdo do Item: Descrição e Status */} +
+
+

+ {item.description} +

+ + Qtd: {item.quantity} + +
+ +
+ + + {/* Visual Diff Badge */} + {!isPending && ( +
+ {isDiff ? ( + + + Nota Alterada + + ) : ( + + + Nota Aceita + + )} +
+ )} +
+
+ + {/* Lado Direito do Item: Campos de Nota */} +
+
+

+ Declarado +

+

+ {declared.toFixed(1)} +

+
+ +
+ +
+

Validado

+
+ + setDraftScores(prev => ({ ...prev, [item.id]: e.target.value })) + } + /> + +
+
+
+
+
+ ); + })} + + + ); + })} +
+ ); +} diff --git a/frontend/src/features/validation/components/ValidationHeader.tsx b/frontend/src/features/validation/components/ValidationHeader.tsx new file mode 100644 index 0000000..01e12b0 --- /dev/null +++ b/frontend/src/features/validation/components/ValidationHeader.tsx @@ -0,0 +1,19 @@ +import { FileText } from 'lucide-react'; + +export function ValidationHeader() { + return ( +
+
+ +

Mesa de Avaliação

+
+

+ Validação de Currículos +

+

+ Analise e valide a pontuação declarada pelos candidatos inscritos nos seus temas de + pesquisa. +

+
+ ); +} diff --git a/frontend/src/features/validation/components/ValidationListing.tsx b/frontend/src/features/validation/components/ValidationListing.tsx new file mode 100644 index 0000000..2eed948 --- /dev/null +++ b/frontend/src/features/validation/components/ValidationListing.tsx @@ -0,0 +1,54 @@ +import { useState } from 'react'; + +import { Card, CardContent } from '@/components/ui/card'; +import { useDebounce } from '@/hooks/use-debounce'; + +import { ValidationHeader } from '@/features/validation/components/ValidationHeader'; +import { ValidationTable } from '@/features/validation/components/ValidationTable'; +import { useValidationCandidates } from '@/features/validation/hooks/use-validation'; + +export function ValidationListing() { + const [searchQuery, setSearchQuery] = useState(''); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + + const debouncedSearch = useDebounce(searchQuery, 500); + + const { data: response, isLoading } = useValidationCandidates({ + page: currentPage, + limit: pageSize, + search: debouncedSearch, + }); + + const candidates = response?.data ?? []; + const totalCandidates = response?.pagination?.total ?? 0; + + const handleSearchChange = (value: string) => { + setSearchQuery(value); + setCurrentPage(1); + }; + + return ( +
+
+ + + + + + + +
+
+ ); +} diff --git a/frontend/src/features/validation/components/ValidationTable.tsx b/frontend/src/features/validation/components/ValidationTable.tsx new file mode 100644 index 0000000..56670d1 --- /dev/null +++ b/frontend/src/features/validation/components/ValidationTable.tsx @@ -0,0 +1,172 @@ +import { Link } from '@tanstack/react-router'; +import type { ColumnDef } from '@tanstack/react-table'; +import { CheckCircle2, ChevronRight, Clock, FileCheck2, Search } from 'lucide-react'; + +import { Table } from '@/components/table'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import type { + CandidateValidationSummary, + ValidationStatus, +} from '@/features/validation/hooks/use-validation'; + +interface ValidationTableProps { + candidates: CandidateValidationSummary[]; + loading: boolean; + searchQuery: string; + totalCandidates: number; + currentPage: number; + pageSize: number; + onSearchQueryChange: (value: string) => void; + onPageChange: (page: number) => void; + onPageSizeChange: (size: number) => void; +} + +function renderBadgeStatus(status: ValidationStatus) { + switch (status) { + case 'completed': + return ( + + Concluído + + ); + case 'in_progress': + return ( + + Em Revisão + + ); + case 'pending': + return ( + + Pendente + + ); + } +} + +function createCandidateColumns(): ColumnDef[] { + return [ + { + accessorKey: 'candidateName', + header: 'CANDIDATO', + cell: ({ row }) => { + const candidate = row.original; + return ( +
+

{candidate.candidateName}

+

{candidate.candidateEmail}

+
+ ); + }, + }, + { + accessorKey: 'themeName', + header: 'TEMA DE PESQUISA', + cell: ({ getValue }) => ( + {getValue()} + ), + }, + { + accessorKey: 'declaredScore', + header: () =>
NOTA DECLARADA
, + cell: ({ getValue }) => ( +
{getValue().toFixed(1)}
+ ), + }, + { + accessorKey: 'validatedScore', + header: () =>
NOTA VALIDADA
, + cell: ({ getValue }) => { + const score = getValue(); + return ( +
+ {score !== null ? score.toFixed(1) : '-'} +
+ ); + }, + }, + { + accessorKey: 'status', + header: () =>
STATUS
, + cell: ({ getValue }) => ( +
{renderBadgeStatus(getValue())}
+ ), + }, + ]; +} + +function createCandidateQuickActions(): ColumnDef[] { + return [ + { + id: 'acoes', + header: () =>
AÇÕES
, + cell: ({ row }) => ( +
+ +
+ ), + }, + ]; +} + +export function ValidationTable({ + candidates, + loading: isLoading, + searchQuery, + totalCandidates, + currentPage, + pageSize, + onSearchQueryChange, + onPageChange, + onPageSizeChange, +}: ValidationTableProps) { + const columns = createCandidateColumns(); + const quickActions = createCandidateQuickActions(); + + return ( +
+
+
+ + onSearchQueryChange(event.target.value)} + /> +
+
+ +
+ + ); +} diff --git a/frontend/src/features/validation/components/ValidationWorkspace.tsx b/frontend/src/features/validation/components/ValidationWorkspace.tsx new file mode 100644 index 0000000..2a6efec --- /dev/null +++ b/frontend/src/features/validation/components/ValidationWorkspace.tsx @@ -0,0 +1,143 @@ +import { useState } from 'react'; + +import { Link, useParams } from '@tanstack/react-router'; +import { ArrowLeft, CheckCircle, FileText, Loader2 } from 'lucide-react'; +import { toast } from 'sonner'; + +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { ValidationForm } from '@/features/validation/components/ValidationForm'; + +export function ValidationWorkspace() { + const { enrollmentId } = useParams({ strict: false }); + const [selectedPdfUrl, setSelectedPdfUrl] = useState(null); + + // Controle do modal de confirmação + const [isConfirmOpen, setIsConfirmOpen] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Simula a finalização da avaliação + const handleFinalize = () => { + setIsSubmitting(true); + // TODO: Trocar pela mutation real do TanStack Query (ex: finalizeValidation.mutate()) + setTimeout(() => { + setIsSubmitting(false); + setIsConfirmOpen(false); + toast.success('Avaliação finalizada com sucesso!'); + // Idealmente, redirecionar de volta para a listagem aqui + }, 1500); + }; + + return ( +
+
+ {/* Header e Navegação */} +
+
+ +

+ Revisão de Currículo +

+

+ Inscrição: {enrollmentId} +

+
+ + +
+ + {/* Layout Split-Screen */} +
+
+ +
+ +
+ +
+ + + Arquivos Anexos + +
+ + {selectedPdfUrl ? ( +