Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
]
}
}
}
94 changes: 94 additions & 0 deletions frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions frontend/src/components/ui/progress.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn('relative h-4 w-full overflow-hidden rounded-full bg-secondary', className)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
));
Progress.displayName = ProgressPrimitive.Root.displayName;

export { Progress };
177 changes: 177 additions & 0 deletions frontend/src/features/validation/components/SecretaryDashboard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="space-y-6">
{/* Dashboard KPIs */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total de Candidatos</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{loadingStats ? (
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
) : (
total
)}
</div>
<p className="text-xs text-muted-foreground">Inscrições submetidas</p>
</CardContent>
</Card>

<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Validações Concluídas</CardTitle>
<CheckCircle2 className="h-4 w-4 text-emerald-600" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-emerald-600">
{loadingStats ? <Loader2 className="h-5 w-5 animate-spin" /> : validated}
</div>
<p className="text-xs text-muted-foreground">Currículos revisados</p>
</CardContent>
</Card>

<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Aguardando Revisão</CardTitle>
<Clock className="h-4 w-4 text-amber-600" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-amber-600">
{loadingStats ? <Loader2 className="h-5 w-5 animate-spin" /> : pending}
</div>
<p className="text-xs text-muted-foreground">Currículos pendentes</p>
</CardContent>
</Card>

<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Progresso Geral</CardTitle>
<TrendingUp className="h-4 w-4 text-primary" />
</CardHeader>
<CardContent className="space-y-3">
<div className="text-2xl font-bold">
{loadingStats ? (
<Loader2 className="h-5 w-5 animate-spin" />
) : (
`${progressPercentage}%`
)}
</div>
<Progress value={progressPercentage} className="h-2" />
</CardContent>
</Card>
</div>

{/* Filters */}
<Card className="shadow-sm">
<CardContent className="p-4">
<div className="flex flex-col gap-4 md:flex-row md:items-center">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="Buscar por nome ou e-mail..."
className="pl-9"
value={search}
onChange={e => setSearch(e.target.value)}
/>
</div>

<div className="flex flex-wrap items-center gap-3">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Filter className="h-4 w-4" />
<span className="hidden lg:inline">Filtros:</span>
</div>

<Select value={level} onValueChange={setLevel}>
<SelectTrigger className="w-35">
<SelectValue placeholder="Nível" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos os Níveis</SelectItem>
<SelectItem value="mestrado">Mestrado</SelectItem>
<SelectItem value="doutorado">Doutorado</SelectItem>
</SelectContent>
</Select>

<Select value={status} onValueChange={setStatus}>
<SelectTrigger className="w-37.5">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos os Status</SelectItem>
<SelectItem value="pending">Pendente</SelectItem>
<SelectItem value="in_progress">Em Revisão</SelectItem>
<SelectItem value="completed">Concluído</SelectItem>
</SelectContent>
</Select>

<Select value={professor} onValueChange={setProfessor}>
<SelectTrigger className="w-45">
<SelectValue placeholder="Professor" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos os Professores</SelectItem>
<SelectItem value="lincoln">Dr. Lincoln</SelectItem>
<SelectItem value="mariana">Dra. Mariana</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>

{/* Candidates Table */}
<SecretaryValidationTable
data={candidatesData?.data ?? []}
loading={loadingCandidates}
currentPage={page}
onPageChange={setPage}
/>
</div>
);
}
Loading
Loading