575 lines
20 KiB
TypeScript
575 lines
20 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { DashboardHeader } from "@/components/dashboard-header";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/components/ui/dialog";
|
|
import {
|
|
Search,
|
|
Filter,
|
|
Eye,
|
|
Mail,
|
|
Phone,
|
|
MapPin,
|
|
Calendar,
|
|
Briefcase,
|
|
CheckCircle2,
|
|
XCircle,
|
|
Clock,
|
|
MessageSquare,
|
|
FileText,
|
|
ExternalLink,
|
|
} from "lucide-react";
|
|
import Link from "next/link";
|
|
|
|
export default function CompanyApplicationsPage() {
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [jobFilter, setJobFilter] = useState("all");
|
|
const [statusFilter, setStatusFilter] = useState("all");
|
|
|
|
const applications = [
|
|
{
|
|
id: "1",
|
|
candidate: {
|
|
id: "1",
|
|
name: "Ana Silva",
|
|
email: "ana.silva@email.com",
|
|
phone: "(11) 98765-4321",
|
|
location: "São Paulo, SP",
|
|
avatar: "",
|
|
experience: "5 anos",
|
|
title: "Desenvolvedora Full Stack",
|
|
},
|
|
jobId: "1",
|
|
jobTitle: "Desenvolvedor Full Stack Sênior",
|
|
status: "pending",
|
|
appliedAt: "2025-11-18T10:30:00",
|
|
resumeUrl: "#",
|
|
},
|
|
{
|
|
id: "2",
|
|
candidate: {
|
|
id: "2",
|
|
name: "Carlos Santos",
|
|
email: "carlos.santos@email.com",
|
|
phone: "(11) 91234-5678",
|
|
location: "Rio de Janeiro, RJ",
|
|
avatar: "",
|
|
experience: "3 anos",
|
|
title: "Designer UX/UI",
|
|
},
|
|
jobId: "2",
|
|
jobTitle: "Designer UX/UI",
|
|
status: "reviewing",
|
|
appliedAt: "2025-11-18T09:15:00",
|
|
resumeUrl: "#",
|
|
},
|
|
{
|
|
id: "3",
|
|
candidate: {
|
|
id: "3",
|
|
name: "Maria Oliveira",
|
|
email: "maria.oliveira@email.com",
|
|
phone: "(21) 99876-5432",
|
|
location: "Belo Horizonte, MG",
|
|
avatar: "",
|
|
experience: "7 anos",
|
|
title: "Engenheira de Dados",
|
|
},
|
|
jobId: "3",
|
|
jobTitle: "Product Manager",
|
|
status: "interview",
|
|
appliedAt: "2025-11-17T14:20:00",
|
|
resumeUrl: "#",
|
|
},
|
|
{
|
|
id: "4",
|
|
candidate: {
|
|
id: "4",
|
|
name: "Pedro Costa",
|
|
email: "pedro.costa@email.com",
|
|
phone: "(31) 98765-1234",
|
|
location: "Curitiba, PR",
|
|
avatar: "",
|
|
experience: "6 anos",
|
|
title: "Product Manager",
|
|
},
|
|
jobId: "1",
|
|
jobTitle: "Desenvolvedor Full Stack Sênior",
|
|
status: "rejected",
|
|
appliedAt: "2025-11-16T16:45:00",
|
|
resumeUrl: "#",
|
|
},
|
|
{
|
|
id: "5",
|
|
candidate: {
|
|
id: "5",
|
|
name: "Juliana Ferreira",
|
|
email: "juliana.ferreira@email.com",
|
|
phone: "(41) 91234-8765",
|
|
location: "Porto Alegre, RS",
|
|
avatar: "",
|
|
experience: "4 anos",
|
|
title: "DevOps Engineer",
|
|
},
|
|
jobId: "2",
|
|
jobTitle: "Designer UX/UI",
|
|
status: "accepted",
|
|
appliedAt: "2025-11-15T11:00:00",
|
|
resumeUrl: "#",
|
|
},
|
|
];
|
|
|
|
const statusConfig = {
|
|
pending: {
|
|
label: "Pendente",
|
|
color: "bg-yellow-500",
|
|
variant: "secondary" as const,
|
|
},
|
|
reviewing: {
|
|
label: "Em Análise",
|
|
color: "bg-blue-500",
|
|
variant: "default" as const,
|
|
},
|
|
interview: {
|
|
label: "Entrevista",
|
|
color: "bg-purple-500",
|
|
variant: "default" as const,
|
|
},
|
|
accepted: {
|
|
label: "Aceito",
|
|
color: "bg-green-500",
|
|
variant: "default" as const,
|
|
},
|
|
rejected: {
|
|
label: "Rejeitado",
|
|
color: "bg-red-500",
|
|
variant: "destructive" as const,
|
|
},
|
|
};
|
|
|
|
const filteredApplications = applications.filter((app) => {
|
|
const matchesSearch =
|
|
app.candidate.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
app.jobTitle.toLowerCase().includes(searchTerm.toLowerCase());
|
|
const matchesJob = jobFilter === "all" || app.jobId === jobFilter;
|
|
const matchesStatus = statusFilter === "all" || app.status === statusFilter;
|
|
|
|
return matchesSearch && matchesJob && matchesStatus;
|
|
});
|
|
|
|
const groupedByStatus = {
|
|
pending: filteredApplications.filter((a) => a.status === "pending"),
|
|
reviewing: filteredApplications.filter((a) => a.status === "reviewing"),
|
|
interview: filteredApplications.filter((a) => a.status === "interview"),
|
|
accepted: filteredApplications.filter((a) => a.status === "accepted"),
|
|
rejected: filteredApplications.filter((a) => a.status === "rejected"),
|
|
};
|
|
|
|
const formatDate = (dateString: string) => {
|
|
const date = new Date(dateString);
|
|
const now = new Date();
|
|
const diffMs = now.getTime() - date.getTime();
|
|
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
const diffDays = Math.floor(diffHours / 24);
|
|
|
|
if (diffHours < 1) return "Agora há pouco";
|
|
if (diffHours < 24) return `Há ${diffHours}h`;
|
|
if (diffDays === 1) return "Ontem";
|
|
if (diffDays < 7) return `Há ${diffDays} dias`;
|
|
return date.toLocaleDateString("pt-BR");
|
|
};
|
|
|
|
const ApplicationCard = ({ app }: { app: (typeof applications)[0] }) => (
|
|
<Card className="hover:shadow-md transition-shadow">
|
|
<CardContent className="p-4 sm:p-6">
|
|
<div className="flex flex-col sm:flex-row gap-4">
|
|
<div className="flex items-start gap-3 flex-1 min-w-0">
|
|
<Avatar className="h-12 w-12 shrink-0">
|
|
<AvatarImage src={app.candidate.avatar} />
|
|
<AvatarFallback className="bg-primary/10 text-primary">
|
|
{app.candidate.name
|
|
.split(" ")
|
|
.map((n) => n[0])
|
|
.join("")
|
|
.toUpperCase()
|
|
.slice(0, 2)}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-start justify-between gap-2 mb-2">
|
|
<div className="min-w-0">
|
|
<h3 className="font-semibold text-base sm:text-lg truncate">
|
|
{app.candidate.name}
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground truncate">
|
|
{app.candidate.title}
|
|
</p>
|
|
</div>
|
|
<Badge
|
|
variant={
|
|
statusConfig[app.status as keyof typeof statusConfig]
|
|
.variant
|
|
}
|
|
className="shrink-0"
|
|
>
|
|
{statusConfig[app.status as keyof typeof statusConfig].label}
|
|
</Badge>
|
|
</div>
|
|
|
|
<div className="space-y-1.5 text-sm text-muted-foreground mb-3">
|
|
<div className="flex items-center gap-2">
|
|
<Briefcase className="h-4 w-4 shrink-0" />
|
|
<span className="truncate">{app.jobTitle}</span>
|
|
</div>
|
|
<div className="flex flex-wrap gap-x-4 gap-y-1">
|
|
<div className="flex items-center gap-1">
|
|
<MapPin className="h-4 w-4 shrink-0" />
|
|
<span className="truncate">{app.candidate.location}</span>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Calendar className="h-4 w-4 shrink-0" />
|
|
<span className="whitespace-nowrap">
|
|
{formatDate(app.appliedAt)}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Clock className="h-4 w-4 shrink-0" />
|
|
<span className="whitespace-nowrap">
|
|
{app.candidate.experience}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
<Dialog>
|
|
<DialogTrigger asChild>
|
|
<Button size="sm" variant="outline">
|
|
<Eye className="h-4 w-4 mr-1" />
|
|
Ver Perfil
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>Perfil do Candidato</DialogTitle>
|
|
<DialogDescription>
|
|
Informações detalhadas sobre {app.candidate.name}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-4">
|
|
<div className="flex items-start gap-4">
|
|
<Avatar className="h-20 w-20">
|
|
<AvatarImage src={app.candidate.avatar} />
|
|
<AvatarFallback className="bg-primary/10 text-primary text-xl">
|
|
{app.candidate.name
|
|
.split(" ")
|
|
.map((n) => n[0])
|
|
.join("")
|
|
.toUpperCase()
|
|
.slice(0, 2)}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div className="flex-1">
|
|
<h3 className="font-bold text-xl">
|
|
{app.candidate.name}
|
|
</h3>
|
|
<p className="text-muted-foreground">
|
|
{app.candidate.title}
|
|
</p>
|
|
<Badge
|
|
className="mt-2"
|
|
variant={
|
|
statusConfig[
|
|
app.status as keyof typeof statusConfig
|
|
].variant
|
|
}
|
|
>
|
|
{
|
|
statusConfig[
|
|
app.status as keyof typeof statusConfig
|
|
].label
|
|
}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 pt-4 border-t">
|
|
<div>
|
|
<Label className="text-xs text-muted-foreground">
|
|
Email
|
|
</Label>
|
|
<p className="text-sm font-medium">
|
|
{app.candidate.email}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs text-muted-foreground">
|
|
Telefone
|
|
</Label>
|
|
<p className="text-sm font-medium">
|
|
{app.candidate.phone}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs text-muted-foreground">
|
|
Localização
|
|
</Label>
|
|
<p className="text-sm font-medium">
|
|
{app.candidate.location}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs text-muted-foreground">
|
|
Experiência
|
|
</Label>
|
|
<p className="text-sm font-medium">
|
|
{app.candidate.experience}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="pt-4 border-t">
|
|
<Label className="text-xs text-muted-foreground mb-2 block">
|
|
Vaga aplicada
|
|
</Label>
|
|
<p className="text-sm font-medium">{app.jobTitle}</p>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
Candidatura enviada em {formatDate(app.appliedAt)}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex flex-col sm:flex-row gap-2 pt-4">
|
|
<Button className="flex-1" asChild>
|
|
<a href={`mailto:${app.candidate.email}`}>
|
|
<Mail className="h-4 w-4 mr-2" />
|
|
Enviar Email
|
|
</a>
|
|
</Button>
|
|
<Button variant="outline" className="flex-1" asChild>
|
|
<a
|
|
href={app.resumeUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
<FileText className="h-4 w-4 mr-2" />
|
|
Ver Currículo
|
|
</a>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<Button size="sm" variant="outline" asChild>
|
|
<a href={`mailto:${app.candidate.email}`}>
|
|
<Mail className="h-4 w-4 mr-1" />
|
|
Contato
|
|
</a>
|
|
</Button>
|
|
|
|
{app.status === "pending" && (
|
|
<>
|
|
<Button size="sm" variant="default">
|
|
<CheckCircle2 className="h-4 w-4 mr-1" />
|
|
Aprovar
|
|
</Button>
|
|
<Button size="sm" variant="destructive">
|
|
<XCircle className="h-4 w-4 mr-1" />
|
|
Rejeitar
|
|
</Button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
|
|
return (
|
|
<div className="min-h-screen bg-background">
|
|
<DashboardHeader />
|
|
|
|
<main className="container mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-8">
|
|
<div className="max-w-7xl mx-auto space-y-6">
|
|
{/* Header */}
|
|
<div>
|
|
<h1 className="text-2xl sm:text-3xl font-bold text-foreground mb-2">
|
|
Candidaturas
|
|
</h1>
|
|
<p className="text-muted-foreground">
|
|
Gerencie todas as candidaturas recebidas
|
|
</p>
|
|
</div>
|
|
|
|
{/* Stats */}
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4">
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="text-2xl font-bold text-foreground">
|
|
{applications.length}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">Total</p>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="text-2xl font-bold text-yellow-600">
|
|
{groupedByStatus.pending.length}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">Pendentes</p>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="text-2xl font-bold text-blue-600">
|
|
{groupedByStatus.reviewing.length}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">Em Análise</p>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="text-2xl font-bold text-purple-600">
|
|
{groupedByStatus.interview.length}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">Entrevistas</p>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="text-2xl font-bold text-green-600">
|
|
{groupedByStatus.accepted.length}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">Aceitos</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
<div className="relative sm:col-span-1">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder="Buscar candidato ou vaga..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="pl-10"
|
|
/>
|
|
</div>
|
|
|
|
<Select value={jobFilter} onValueChange={setJobFilter}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Filtrar por vaga" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">Todas as vagas</SelectItem>
|
|
<SelectItem value="1">
|
|
Desenvolvedor Full Stack Sênior
|
|
</SelectItem>
|
|
<SelectItem value="2">Designer UX/UI</SelectItem>
|
|
<SelectItem value="3">Product Manager</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Filtrar por status" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">Todos os status</SelectItem>
|
|
<SelectItem value="pending">Pendente</SelectItem>
|
|
<SelectItem value="reviewing">Em Análise</SelectItem>
|
|
<SelectItem value="interview">Entrevista</SelectItem>
|
|
<SelectItem value="accepted">Aceito</SelectItem>
|
|
<SelectItem value="rejected">Rejeitado</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Applications by Status */}
|
|
<Tabs defaultValue="all" className="space-y-4">
|
|
<TabsList className="grid w-full grid-cols-3 sm:grid-cols-6">
|
|
<TabsTrigger value="all">Todas</TabsTrigger>
|
|
<TabsTrigger value="pending">Pendentes</TabsTrigger>
|
|
<TabsTrigger value="reviewing">Em Análise</TabsTrigger>
|
|
<TabsTrigger value="interview">Entrevista</TabsTrigger>
|
|
<TabsTrigger value="accepted">Aceitos</TabsTrigger>
|
|
<TabsTrigger value="rejected">Rejeitados</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="all" className="space-y-4">
|
|
{filteredApplications.map((app) => (
|
|
<ApplicationCard key={app.id} app={app} />
|
|
))}
|
|
</TabsContent>
|
|
|
|
{Object.entries(groupedByStatus).map(([status, apps]) => (
|
|
<TabsContent key={status} value={status} className="space-y-4">
|
|
{apps.length > 0 ? (
|
|
apps.map((app) => <ApplicationCard key={app.id} app={app} />)
|
|
) : (
|
|
<Card>
|
|
<CardContent className="flex flex-col items-center justify-center py-12">
|
|
<MessageSquare className="h-12 w-12 text-muted-foreground mb-4" />
|
|
<p className="text-muted-foreground">
|
|
Nenhuma candidatura com status "
|
|
{
|
|
statusConfig[status as keyof typeof statusConfig]
|
|
.label
|
|
}
|
|
"
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</TabsContent>
|
|
))}
|
|
</Tabs>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Label({
|
|
children,
|
|
className,
|
|
}: {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}) {
|
|
return <label className={className}>{children}</label>;
|
|
}
|