diff --git a/frontend/package.json b/frontend/package.json index 0957201..f3dc076 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,6 +19,7 @@ "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-progress": "^1.1.10", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", @@ -66,5 +67,23 @@ "typescript": "~6.0.3", "typescript-eslint": "^8.56.1", "vite": "^8.0.5" + }, + "pnpm": { + "supportedArchitectures": { + "os": [ + "current", + "linux" + ], + "cpu": [ + "current", + "x64", + "arm64" + ], + "libc": [ + "current", + "glibc", + "musl" + ] + } } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 47187ce..32ee050 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@radix-ui/react-popover': specifier: ^1.1.15 version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-progress': + specifier: ^1.1.10 + version: 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@radix-ui/react-select': specifier: ^2.2.6 version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -413,6 +416,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-compose-refs@1.1.3': + resolution: {integrity: sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-context@1.1.2': resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} peerDependencies: @@ -431,6 +443,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-context@1.1.4': + resolution: {integrity: sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-dialog@1.1.15': resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} peerDependencies: @@ -614,6 +635,32 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-primitive@2.1.6': + resolution: {integrity: sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-progress@1.1.10': + resolution: {integrity: sha512-JYzEg60lk79PwKM27WZyKd7PW8O4OM5jOaFfRPfOyeXmMw7tLJh5kSj+CEjVTehszuwml/AdCzPGMXBTGf4BBw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-roving-focus@1.1.11': resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} peerDependencies: @@ -671,6 +718,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-slot@1.3.0': + resolution: {integrity: sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-tooltip@1.2.8': resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} peerDependencies: @@ -2512,6 +2568,12 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + '@radix-ui/react-compose-refs@1.1.3(@types/react@19.2.14)(react@19.2.6)': + dependencies: + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 + '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.6)': dependencies: react: 19.2.6 @@ -2524,6 +2586,12 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + '@radix-ui/react-context@1.1.4(@types/react@19.2.14)(react@19.2.6)': + dependencies: + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -2718,6 +2786,25 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-primitive@2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/react-slot': 1.3.0(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-progress@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -2787,6 +2874,13 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + '@radix-ui/react-slot@1.3.0(@types/react@19.2.14)(react@19.2.6)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 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 ? ( +