761 lines
35 KiB
TypeScript
761 lines
35 KiB
TypeScript
"use client"
|
|
|
|
import { useState } from "react"
|
|
import { mockCandidates, mockRequisitions, mockUsers, pipelineStages } from "@/lib/mock-data"
|
|
import type { Candidate, PipelineStage } from "@/lib/types"
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
|
|
import { Label } from "@/components/ui/label"
|
|
import { Textarea } from "@/components/ui/textarea"
|
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
|
import {
|
|
Search,
|
|
Filter,
|
|
Plus,
|
|
MoreVertical,
|
|
Mail,
|
|
Phone,
|
|
Calendar,
|
|
Star,
|
|
ChevronRight,
|
|
FileText,
|
|
MessageSquare,
|
|
Clock,
|
|
CheckCircle2,
|
|
XCircle,
|
|
ArrowRight,
|
|
GripVertical,
|
|
User,
|
|
Briefcase,
|
|
MapPin,
|
|
DollarSign,
|
|
ThumbsUp,
|
|
ThumbsDown,
|
|
Eye,
|
|
Download,
|
|
Send,
|
|
Video,
|
|
Linkedin
|
|
} from "lucide-react"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
export function RecruitmentPipelinePage() {
|
|
const [candidates, setCandidates] = useState<Candidate[]>(mockCandidates)
|
|
const [selectedRequisition, setSelectedRequisition] = useState<string>("all")
|
|
const [searchQuery, setSearchQuery] = useState("")
|
|
const [selectedCandidate, setSelectedCandidate] = useState<Candidate | null>(null)
|
|
const [isDetailOpen, setIsDetailOpen] = useState(false)
|
|
const [isAddCandidateOpen, setIsAddCandidateOpen] = useState(false)
|
|
const [draggedCandidate, setDraggedCandidate] = useState<Candidate | null>(null)
|
|
const [viewMode, setViewMode] = useState<"kanban" | "list">("kanban")
|
|
|
|
const activeRequisitions = mockRequisitions.filter(r => r.status === "approved" || r.status === "published")
|
|
|
|
const filteredCandidates = candidates.filter(candidate => {
|
|
const matchesRequisition = selectedRequisition === "all" || candidate.requisitionId === selectedRequisition
|
|
const matchesSearch = candidate.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
candidate.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
candidate.currentPosition?.toLowerCase().includes(searchQuery.toLowerCase())
|
|
return matchesRequisition && matchesSearch
|
|
})
|
|
|
|
const getCandidatesByStage = (stageId: string) => {
|
|
return filteredCandidates.filter(c => c.currentStage === stageId)
|
|
}
|
|
|
|
const handleDragStart = (candidate: Candidate) => {
|
|
setDraggedCandidate(candidate)
|
|
}
|
|
|
|
const handleDragOver = (e: React.DragEvent) => {
|
|
e.preventDefault()
|
|
}
|
|
|
|
const handleDrop = (stageId: string) => {
|
|
if (draggedCandidate) {
|
|
setCandidates(prev => prev.map(c =>
|
|
c.id === draggedCandidate.id
|
|
? { ...c, currentStage: stageId as PipelineStage }
|
|
: c
|
|
))
|
|
setDraggedCandidate(null)
|
|
}
|
|
}
|
|
|
|
const getStageColor = (stageId: string) => {
|
|
const colors: Record<string, string> = {
|
|
applied: "border-slate-300",
|
|
screening: "border-[var(--color-chart-1)]",
|
|
interview_rrhh: "border-[var(--color-chart-2)]",
|
|
interview_technical: "border-[var(--color-chart-3)]",
|
|
interview_manager: "border-[var(--color-chart-4)]",
|
|
offer: "border-[var(--color-status-recruiting)]",
|
|
hired: "border-[var(--color-status-closed)]",
|
|
rejected: "border-[var(--color-status-cancelled)]"
|
|
}
|
|
return colors[stageId] || "border-muted-foreground"
|
|
}
|
|
|
|
const getRatingStars = (rating: number) => {
|
|
return Array.from({ length: 5 }, (_, i) => (
|
|
<Star
|
|
key={i}
|
|
className={cn(
|
|
"h-3 w-3",
|
|
i < rating ? "fill-yellow-400 text-yellow-400" : "text-muted-foreground/30"
|
|
)}
|
|
/>
|
|
))
|
|
}
|
|
|
|
const getSourceBadge = (source: string) => {
|
|
const sourceConfig: Record<string, { label: string; className: string }> = {
|
|
linkedin: { label: "LinkedIn", className: "bg-[var(--color-chart-2)] text-white" },
|
|
referral: { label: "Referido", className: "bg-accent text-accent-foreground" },
|
|
job_board: { label: "Portal Empleo", className: "bg-primary text-primary-foreground" },
|
|
website: { label: "Web", className: "bg-secondary text-secondary-foreground" },
|
|
agency: { label: "Agencia", className: "bg-[var(--color-chart-1)] text-white" }
|
|
}
|
|
const config = sourceConfig[source] || { label: source, className: "bg-muted text-muted-foreground" }
|
|
return <Badge className={config.className}>{config.label}</Badge>
|
|
}
|
|
|
|
const openCandidateDetail = (candidate: Candidate) => {
|
|
setSelectedCandidate(candidate)
|
|
setIsDetailOpen(true)
|
|
}
|
|
|
|
const getRequisitionTitle = (requisitionId: string) => {
|
|
const requisition = mockRequisitions.find(r => r.id === requisitionId)
|
|
return requisition?.position || "Sin asignar"
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold tracking-tight">Pipeline de Reclutamiento</h1>
|
|
<p className="text-muted-foreground">
|
|
Gestiona candidatos y su progreso en el proceso de seleccion
|
|
</p>
|
|
</div>
|
|
<Dialog open={isAddCandidateOpen} onOpenChange={setIsAddCandidateOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button className="bg-accent hover:bg-accent/90 text-accent-foreground">
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Agregar Candidato
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="max-w-2xl">
|
|
<DialogHeader>
|
|
<DialogTitle>Agregar Nuevo Candidato</DialogTitle>
|
|
<DialogDescription>
|
|
Ingresa la informacion del candidato para agregarlo al pipeline
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="grid gap-4 py-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="name">Nombre Completo</Label>
|
|
<Input id="name" placeholder="Juan Perez" />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="email">Email</Label>
|
|
<Input id="email" type="email" placeholder="juan@email.com" />
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="phone">Telefono</Label>
|
|
<Input id="phone" placeholder="+52 55 1234 5678" />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="linkedin">LinkedIn</Label>
|
|
<Input id="linkedin" placeholder="linkedin.com/in/usuario" />
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="position">Posicion Actual</Label>
|
|
<Input id="position" placeholder="Ingeniero de Software" />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="company">Empresa Actual</Label>
|
|
<Input id="company" placeholder="Empresa ABC" />
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="requisition">Vacante</Label>
|
|
<Select>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Seleccionar vacante" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{activeRequisitions.map(req => (
|
|
<SelectItem key={req.id} value={req.id}>
|
|
{req.position} - {req.department}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="source">Fuente</Label>
|
|
<Select>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Seleccionar fuente" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="linkedin">LinkedIn</SelectItem>
|
|
<SelectItem value="referral">Referido</SelectItem>
|
|
<SelectItem value="job_board">Portal de Empleo</SelectItem>
|
|
<SelectItem value="website">Sitio Web</SelectItem>
|
|
<SelectItem value="agency">Agencia</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="salary">Expectativa Salarial</Label>
|
|
<Input id="salary" placeholder="$50,000 MXN" />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="availability">Disponibilidad</Label>
|
|
<Select>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Seleccionar" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="immediate">Inmediata</SelectItem>
|
|
<SelectItem value="2_weeks">2 Semanas</SelectItem>
|
|
<SelectItem value="1_month">1 Mes</SelectItem>
|
|
<SelectItem value="negotiable">Negociable</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="notes">Notas</Label>
|
|
<Textarea id="notes" placeholder="Notas adicionales sobre el candidato..." />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="cv">CV / Resume</Label>
|
|
<Input id="cv" type="file" accept=".pdf,.doc,.docx" />
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setIsAddCandidateOpen(false)}>
|
|
Cancelar
|
|
</Button>
|
|
<Button className="bg-accent hover:bg-accent/90" onClick={() => setIsAddCandidateOpen(false)}>
|
|
Agregar Candidato
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<Card>
|
|
<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 candidatos por nombre, email o posicion..."
|
|
className="pl-10"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
/>
|
|
</div>
|
|
<Select value={selectedRequisition} onValueChange={setSelectedRequisition}>
|
|
<SelectTrigger className="w-full md:w-[280px]">
|
|
<SelectValue placeholder="Filtrar por vacante" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">Todas las vacantes</SelectItem>
|
|
{activeRequisitions.map(req => (
|
|
<SelectItem key={req.id} value={req.id}>
|
|
{req.position} - {req.department}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant={viewMode === "kanban" ? "default" : "outline"}
|
|
size="sm"
|
|
onClick={() => setViewMode("kanban")}
|
|
>
|
|
Kanban
|
|
</Button>
|
|
<Button
|
|
variant={viewMode === "list" ? "default" : "outline"}
|
|
size="sm"
|
|
onClick={() => setViewMode("list")}
|
|
>
|
|
Lista
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Pipeline Stats */}
|
|
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-8 gap-3">
|
|
{pipelineStages.map(stage => {
|
|
const count = getCandidatesByStage(stage.id).length
|
|
return (
|
|
<div
|
|
key={stage.id}
|
|
className={`p-4 rounded-xl border bg-white ${getStageColor(stage.id)} border-t-4 shadow-sm flex flex-col justify-center`}
|
|
>
|
|
<div className="text-2xl font-bold mb-1 text-card-foreground">{count}</div>
|
|
<div className="text-sm font-medium text-muted-foreground">{stage.name}</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{/* Kanban Board */}
|
|
{viewMode === "kanban" ? (
|
|
<div className="flex gap-4 overflow-x-auto pb-4">
|
|
{pipelineStages.filter(s => s.id !== "rejected").map(stage => {
|
|
const columnCandidates = getCandidatesByStage(stage.id)
|
|
return (
|
|
<div
|
|
key={stage.id}
|
|
className="flex-shrink-0 w-[300px]"
|
|
onDragOver={handleDragOver}
|
|
onDrop={() => handleDrop(stage.id)}
|
|
>
|
|
<div className={`flex flex-col h-full min-h-[500px] rounded-lg border bg-white ${getStageColor(stage.id)} border-t-4`}>
|
|
<div className="flex items-center justify-between p-3 border-b border-border/50">
|
|
<h3 className="font-semibold text-sm text-card-foreground">
|
|
{stage.name}
|
|
</h3>
|
|
<Badge variant="secondary" className="bg-muted text-muted-foreground">{columnCandidates.length}</Badge>
|
|
</div>
|
|
<div className="space-y-3 p-3 max-h-[600px] overflow-y-auto">
|
|
{columnCandidates.map(candidate => (
|
|
<Card
|
|
key={candidate.id}
|
|
className="cursor-pointer hover:shadow-md transition-shadow"
|
|
draggable
|
|
onDragStart={() => handleDragStart(candidate)}
|
|
onClick={() => openCandidateDetail(candidate)}
|
|
>
|
|
<CardContent className="p-3">
|
|
<div className="flex items-start gap-3">
|
|
<Avatar className="h-10 w-10">
|
|
<AvatarImage src={candidate.avatar} />
|
|
<AvatarFallback className="bg-primary/10 text-primary">
|
|
{candidate.name.split(" ").map(n => n[0]).join("")}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2">
|
|
<h4 className="font-medium text-sm truncate">{candidate.name}</h4>
|
|
<GripVertical className="h-3 w-3 text-muted-foreground/50" />
|
|
</div>
|
|
<p className="text-xs text-muted-foreground truncate">
|
|
{candidate.currentPosition}
|
|
</p>
|
|
<div className="flex items-center gap-1 mt-1">
|
|
{getRatingStars(candidate.rating || 0)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between mt-3">
|
|
{getSourceBadge(candidate.source)}
|
|
<span className="text-xs text-muted-foreground">
|
|
{new Date(candidate.appliedDate).toLocaleDateString("es-MX", {
|
|
day: "numeric",
|
|
month: "short"
|
|
})}
|
|
</span>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
{columnCandidates.length === 0 && (
|
|
<div className="text-center py-8 text-muted-foreground text-sm">
|
|
Sin candidatos
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)})}
|
|
</div>
|
|
) : (
|
|
/* List View */
|
|
<Card>
|
|
<CardContent className="p-0">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead>
|
|
<tr className="border-b bg-muted/50">
|
|
<th className="text-left p-4 font-medium">Candidato</th>
|
|
<th className="text-left p-4 font-medium">Vacante</th>
|
|
<th className="text-left p-4 font-medium">Etapa</th>
|
|
<th className="text-left p-4 font-medium">Fuente</th>
|
|
<th className="text-left p-4 font-medium">Rating</th>
|
|
<th className="text-left p-4 font-medium">Fecha</th>
|
|
<th className="text-right p-4 font-medium">Acciones</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{filteredCandidates.map(candidate => {
|
|
const stage = pipelineStages.find(s => s.id === candidate.currentStage)
|
|
return (
|
|
<tr
|
|
key={candidate.id}
|
|
className="border-b hover:bg-muted/30 cursor-pointer"
|
|
onClick={() => openCandidateDetail(candidate)}
|
|
>
|
|
<td className="p-4">
|
|
<div className="flex items-center gap-3">
|
|
<Avatar className="h-9 w-9">
|
|
<AvatarImage src={candidate.avatar} />
|
|
<AvatarFallback className="bg-primary/10 text-primary text-xs">
|
|
{candidate.name.split(" ").map(n => n[0]).join("")}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div>
|
|
<div className="font-medium">{candidate.name}</div>
|
|
<div className="text-sm text-muted-foreground">{candidate.email}</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="p-4">
|
|
<span className="text-sm">{getRequisitionTitle(candidate.requisitionId)}</span>
|
|
</td>
|
|
<td className="p-4">
|
|
<Badge variant="outline" className={cn("border", getStageColor(candidate.currentStage))}>
|
|
{stage?.name}
|
|
</Badge>
|
|
</td>
|
|
<td className="p-4">
|
|
{getSourceBadge(candidate.source)}
|
|
</td>
|
|
<td className="p-4">
|
|
<div className="flex items-center gap-1">
|
|
{getRatingStars(candidate.rating || 0)}
|
|
</div>
|
|
</td>
|
|
<td className="p-4">
|
|
<span className="text-sm text-muted-foreground">
|
|
{new Date(candidate.appliedDate).toLocaleDateString("es-MX")}
|
|
</span>
|
|
</td>
|
|
<td className="p-4 text-right">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
|
|
<Button variant="ghost" size="icon">
|
|
<MoreVertical className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem>
|
|
<Eye className="mr-2 h-4 w-4" />
|
|
Ver Perfil
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem>
|
|
<Mail className="mr-2 h-4 w-4" />
|
|
Enviar Email
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem>
|
|
<Calendar className="mr-2 h-4 w-4" />
|
|
Agendar Entrevista
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem>
|
|
<ArrowRight className="mr-2 h-4 w-4" />
|
|
Mover a siguiente etapa
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem className="text-destructive">
|
|
<XCircle className="mr-2 h-4 w-4" />
|
|
Rechazar
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</td>
|
|
</tr>
|
|
)
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Candidate Detail Dialog */}
|
|
<Dialog open={isDetailOpen} onOpenChange={setIsDetailOpen}>
|
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
|
{selectedCandidate && (
|
|
<>
|
|
<DialogHeader>
|
|
<div className="flex items-start gap-4">
|
|
<Avatar className="h-16 w-16">
|
|
<AvatarImage src={selectedCandidate.avatar} />
|
|
<AvatarFallback className="bg-primary/10 text-primary text-xl">
|
|
{selectedCandidate.name.split(" ").map(n => n[0]).join("")}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div className="flex-1">
|
|
<DialogTitle className="text-xl">{selectedCandidate.name}</DialogTitle>
|
|
<DialogDescription className="mt-1">
|
|
{selectedCandidate.currentPosition} en {selectedCandidate.currentCompany}
|
|
</DialogDescription>
|
|
<div className="flex items-center gap-3 mt-2">
|
|
{getSourceBadge(selectedCandidate.source)}
|
|
<Badge variant="outline">
|
|
{pipelineStages.find(s => s.id === selectedCandidate.currentStage)?.name}
|
|
</Badge>
|
|
<div className="flex items-center gap-1">
|
|
{getRatingStars(selectedCandidate.rating || 0)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button variant="outline" size="icon">
|
|
<Linkedin className="h-4 w-4" />
|
|
</Button>
|
|
<Button variant="outline" size="icon">
|
|
<Mail className="h-4 w-4" />
|
|
</Button>
|
|
<Button variant="outline" size="icon">
|
|
<Phone className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogHeader>
|
|
|
|
<Tabs defaultValue="info" className="mt-4">
|
|
<TabsList className="grid w-full grid-cols-4">
|
|
<TabsTrigger value="info">Informacion</TabsTrigger>
|
|
<TabsTrigger value="timeline">Timeline</TabsTrigger>
|
|
<TabsTrigger value="evaluations">Evaluaciones</TabsTrigger>
|
|
<TabsTrigger value="documents">Documentos</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="info" className="space-y-4 mt-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm">Contacto</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2">
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Mail className="h-4 w-4 text-muted-foreground" />
|
|
{selectedCandidate.email}
|
|
</div>
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Phone className="h-4 w-4 text-muted-foreground" />
|
|
{selectedCandidate.phone}
|
|
</div>
|
|
{selectedCandidate.linkedin && (
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Linkedin className="h-4 w-4 text-muted-foreground" />
|
|
<a href={selectedCandidate.linkedin} className="text-primary hover:underline">
|
|
Ver perfil
|
|
</a>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm">Expectativas</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2">
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
|
{selectedCandidate.salaryExpectation || "No especificado"}
|
|
</div>
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Clock className="h-4 w-4 text-muted-foreground" />
|
|
Disponibilidad: {selectedCandidate.availability || "No especificada"}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm">Vacante Aplicada</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<div className="font-medium">{getRequisitionTitle(selectedCandidate.requisitionId)}</div>
|
|
<div className="text-sm text-muted-foreground">
|
|
Aplicado el {new Date(selectedCandidate.appliedDate).toLocaleDateString("es-MX", {
|
|
day: "numeric",
|
|
month: "long",
|
|
year: "numeric"
|
|
})}
|
|
</div>
|
|
</div>
|
|
<Button variant="outline" size="sm">
|
|
Ver Requisicion
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{selectedCandidate.notes && (
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm">Notas</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-sm text-muted-foreground">{selectedCandidate.notes}</p>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</TabsContent>
|
|
|
|
<TabsContent value="timeline" className="mt-4">
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="space-y-4">
|
|
{selectedCandidate.history?.map((event, index) => (
|
|
<div key={index} className="flex gap-4">
|
|
<div className="flex flex-col items-center">
|
|
<div className={cn(
|
|
"w-8 h-8 rounded-full flex items-center justify-center",
|
|
event.type === "stage_change" && "bg-primary/10 text-primary",
|
|
event.type === "note" && "bg-secondary/10 text-secondary",
|
|
event.type === "interview" && "bg-accent/10 text-accent",
|
|
event.type === "evaluation" && "bg-blue-500/10 text-blue-500"
|
|
)}>
|
|
{event.type === "stage_change" && <ArrowRight className="h-4 w-4" />}
|
|
{event.type === "note" && <MessageSquare className="h-4 w-4" />}
|
|
{event.type === "interview" && <Video className="h-4 w-4" />}
|
|
{event.type === "evaluation" && <Star className="h-4 w-4" />}
|
|
</div>
|
|
{index < (selectedCandidate.history?.length || 0) - 1 && (
|
|
<div className="w-px h-full bg-border mt-2" />
|
|
)}
|
|
</div>
|
|
<div className="flex-1 pb-4">
|
|
<div className="flex items-center justify-between">
|
|
<h4 className="font-medium text-sm">{event.title}</h4>
|
|
<span className="text-xs text-muted-foreground">
|
|
{new Date(event.date).toLocaleDateString("es-MX")}
|
|
</span>
|
|
</div>
|
|
<p className="text-sm text-muted-foreground mt-1">{event.description}</p>
|
|
{event.user && (
|
|
<p className="text-xs text-muted-foreground mt-1">Por: {event.user}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="evaluations" className="mt-4">
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="space-y-4">
|
|
{selectedCandidate.evaluations?.map((evaluation, index) => (
|
|
<Card key={index}>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<h4 className="font-medium">{evaluation.stage}</h4>
|
|
<p className="text-sm text-muted-foreground">
|
|
Evaluado por {evaluation.evaluator} - {new Date(evaluation.date).toLocaleDateString("es-MX")}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Badge variant={evaluation.recommendation === "hire" ? "default" : evaluation.recommendation === "reject" ? "destructive" : "secondary"}>
|
|
{evaluation.recommendation === "hire" ? "Contratar" : evaluation.recommendation === "reject" ? "Rechazar" : "Continuar"}
|
|
</Badge>
|
|
<div className="flex items-center gap-1">
|
|
{getRatingStars(evaluation.score)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p className="text-sm mt-3">{evaluation.comments}</p>
|
|
</CardContent>
|
|
</Card>
|
|
)) || (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
No hay evaluaciones registradas
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="documents" className="mt-4">
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="space-y-3">
|
|
{selectedCandidate.documents?.map((doc, index) => (
|
|
<div key={index} className="flex items-center justify-between p-3 border rounded-lg">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center">
|
|
<FileText className="h-5 w-5 text-primary" />
|
|
</div>
|
|
<div>
|
|
<div className="font-medium text-sm">{doc.name}</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
Subido el {new Date(doc.uploadedAt).toLocaleDateString("es-MX")}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Button variant="ghost" size="icon">
|
|
<Download className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
)) || (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
No hay documentos adjuntos
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
|
|
<DialogFooter className="gap-2 mt-4">
|
|
<Button variant="outline">
|
|
<XCircle className="mr-2 h-4 w-4" />
|
|
Rechazar
|
|
</Button>
|
|
<Button variant="outline">
|
|
<Calendar className="mr-2 h-4 w-4" />
|
|
Agendar Entrevista
|
|
</Button>
|
|
<Button className="bg-accent hover:bg-accent/90">
|
|
<ArrowRight className="mr-2 h-4 w-4" />
|
|
Siguiente Etapa
|
|
</Button>
|
|
</DialogFooter>
|
|
</>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
)
|
|
}
|