529 lines
22 KiB
TypeScript
529 lines
22 KiB
TypeScript
"use client"
|
|
|
|
import { useState } from "react"
|
|
import { mockRequisitions, mockCandidates, mockDepartments } from "@/lib/mock-data"
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Progress } from "@/components/ui/progress"
|
|
import {
|
|
BarChart3,
|
|
TrendingUp,
|
|
TrendingDown,
|
|
Download,
|
|
Calendar,
|
|
Users,
|
|
Clock,
|
|
Target,
|
|
Building2,
|
|
Briefcase,
|
|
CheckCircle2,
|
|
XCircle,
|
|
PieChart,
|
|
Activity,
|
|
ArrowUpRight,
|
|
ArrowDownRight,
|
|
FileText,
|
|
Filter
|
|
} from "lucide-react"
|
|
import {
|
|
BarChart,
|
|
Bar,
|
|
XAxis,
|
|
YAxis,
|
|
CartesianGrid,
|
|
Tooltip,
|
|
Legend,
|
|
ResponsiveContainer,
|
|
LineChart,
|
|
Line,
|
|
PieChart as RechartsPieChart,
|
|
Pie,
|
|
Cell,
|
|
AreaChart,
|
|
Area
|
|
} from "recharts"
|
|
|
|
// Sample data for charts
|
|
const requisitionsByMonth = [
|
|
{ month: "Ene", creadas: 12, aprobadas: 10, cubiertas: 8 },
|
|
{ month: "Feb", creadas: 15, aprobadas: 13, cubiertas: 11 },
|
|
{ month: "Mar", creadas: 18, aprobadas: 16, cubiertas: 14 },
|
|
{ month: "Abr", creadas: 14, aprobadas: 12, cubiertas: 10 },
|
|
{ month: "May", creadas: 20, aprobadas: 18, cubiertas: 15 },
|
|
{ month: "Jun", creadas: 16, aprobadas: 14, cubiertas: 12 }
|
|
]
|
|
|
|
const candidatesBySource = [
|
|
{ name: "LinkedIn", value: 45, color: "#0077B5" },
|
|
{ name: "Referidos", value: 25, color: "#61ce70" },
|
|
{ name: "Portal Empleo", value: 18, color: "#6ec1e4" },
|
|
{ name: "Website", value: 8, color: "#4054b2" },
|
|
{ name: "Agencias", value: 4, color: "#54595f" }
|
|
]
|
|
|
|
const timeToHire = [
|
|
{ month: "Ene", dias: 35 },
|
|
{ month: "Feb", dias: 32 },
|
|
{ month: "Mar", dias: 28 },
|
|
{ month: "Abr", dias: 30 },
|
|
{ month: "May", dias: 25 },
|
|
{ month: "Jun", dias: 22 }
|
|
]
|
|
|
|
const requisitionsByDepartment = [
|
|
{ name: "Tecnologia", abiertas: 8, cubiertas: 15 },
|
|
{ name: "Ventas", abiertas: 5, cubiertas: 12 },
|
|
{ name: "Marketing", abiertas: 3, cubiertas: 8 },
|
|
{ name: "Operaciones", abiertas: 4, cubiertas: 10 },
|
|
{ name: "Finanzas", abiertas: 2, cubiertas: 6 },
|
|
{ name: "RR.HH.", abiertas: 1, cubiertas: 4 }
|
|
]
|
|
|
|
const pipelineConversion = [
|
|
{ stage: "Aplicados", count: 500, percentage: 100 },
|
|
{ stage: "Screening", count: 250, percentage: 50 },
|
|
{ stage: "Entrevista RH", count: 120, percentage: 24 },
|
|
{ stage: "Entrevista Tec", count: 60, percentage: 12 },
|
|
{ stage: "Entrevista Ger", count: 30, percentage: 6 },
|
|
{ stage: "Oferta", count: 15, percentage: 3 },
|
|
{ stage: "Contratados", count: 12, percentage: 2.4 }
|
|
]
|
|
|
|
export function ReportsPage() {
|
|
const [dateRange, setDateRange] = useState("6m")
|
|
const [selectedDepartment, setSelectedDepartment] = useState("all")
|
|
|
|
// Calculate stats
|
|
const totalRequisitions = mockRequisitions.length
|
|
const openRequisitions = mockRequisitions.filter(r =>
|
|
["draft", "pending", "approved", "published"].includes(r.status)
|
|
).length
|
|
const closedRequisitions = mockRequisitions.filter(r =>
|
|
["filled", "cancelled"].includes(r.status)
|
|
).length
|
|
const totalCandidates = mockCandidates.length
|
|
const hiredCandidates = mockCandidates.filter(c => c.currentStage === "hired").length
|
|
const avgTimeToHire = 27 // days
|
|
|
|
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">Reportes y Analitica</h1>
|
|
<p className="text-muted-foreground">
|
|
Metricas y estadisticas del proceso de reclutamiento
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<Select value={dateRange} onValueChange={setDateRange}>
|
|
<SelectTrigger className="w-[180px]">
|
|
<SelectValue placeholder="Periodo" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="1m">Ultimo mes</SelectItem>
|
|
<SelectItem value="3m">Ultimos 3 meses</SelectItem>
|
|
<SelectItem value="6m">Ultimos 6 meses</SelectItem>
|
|
<SelectItem value="1y">Ultimo ano</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<Button variant="outline">
|
|
<Download className="mr-2 h-4 w-4" />
|
|
Exportar
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* KPIs */}
|
|
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<Briefcase className="h-5 w-5 text-primary" />
|
|
<Badge variant="secondary" className="text-xs">
|
|
<ArrowUpRight className="h-3 w-3 mr-1" />
|
|
12%
|
|
</Badge>
|
|
</div>
|
|
<div className="text-2xl font-bold">{totalRequisitions}</div>
|
|
<div className="text-xs text-muted-foreground">Total Requisiciones</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<Target className="h-5 w-5 text-accent" />
|
|
<Badge className="bg-accent/10 text-accent text-xs">Activas</Badge>
|
|
</div>
|
|
<div className="text-2xl font-bold">{openRequisitions}</div>
|
|
<div className="text-xs text-muted-foreground">Vacantes Abiertas</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<Users className="h-5 w-5 text-blue-500" />
|
|
<Badge variant="secondary" className="text-xs">
|
|
<ArrowUpRight className="h-3 w-3 mr-1" />
|
|
8%
|
|
</Badge>
|
|
</div>
|
|
<div className="text-2xl font-bold">{totalCandidates}</div>
|
|
<div className="text-xs text-muted-foreground">Total Candidatos</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<CheckCircle2 className="h-5 w-5 text-accent" />
|
|
<Badge className="bg-accent/10 text-accent text-xs">
|
|
<ArrowUpRight className="h-3 w-3 mr-1" />
|
|
15%
|
|
</Badge>
|
|
</div>
|
|
<div className="text-2xl font-bold">{hiredCandidates}</div>
|
|
<div className="text-xs text-muted-foreground">Contratados</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<Clock className="h-5 w-5 text-secondary" />
|
|
<Badge variant="secondary" className="text-xs">
|
|
<ArrowDownRight className="h-3 w-3 mr-1 text-accent" />
|
|
-5 dias
|
|
</Badge>
|
|
</div>
|
|
<div className="text-2xl font-bold">{avgTimeToHire}d</div>
|
|
<div className="text-xs text-muted-foreground">Tiempo Promedio</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<Activity className="h-5 w-5 text-purple-500" />
|
|
</div>
|
|
<div className="text-2xl font-bold">2.4%</div>
|
|
<div className="text-xs text-muted-foreground">Tasa Conversion</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<Tabs defaultValue="overview" className="space-y-6">
|
|
<TabsList>
|
|
<TabsTrigger value="overview">Vision General</TabsTrigger>
|
|
<TabsTrigger value="requisitions">Requisiciones</TabsTrigger>
|
|
<TabsTrigger value="candidates">Candidatos</TabsTrigger>
|
|
<TabsTrigger value="pipeline">Pipeline</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="overview" className="space-y-6">
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{/* Requisitions Trend */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">Tendencia de Requisiciones</CardTitle>
|
|
<CardDescription>Requisiciones creadas, aprobadas y cubiertas</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<BarChart data={requisitionsByMonth}>
|
|
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
|
<XAxis dataKey="month" className="text-xs" />
|
|
<YAxis className="text-xs" />
|
|
<Tooltip
|
|
contentStyle={{
|
|
backgroundColor: "hsl(var(--card))",
|
|
border: "1px solid hsl(var(--border))",
|
|
borderRadius: "8px"
|
|
}}
|
|
/>
|
|
<Legend />
|
|
<Bar dataKey="creadas" fill="#6ec1e4" name="Creadas" radius={[4, 4, 0, 0]} />
|
|
<Bar dataKey="aprobadas" fill="#4054b2" name="Aprobadas" radius={[4, 4, 0, 0]} />
|
|
<Bar dataKey="cubiertas" fill="#61ce70" name="Cubiertas" radius={[4, 4, 0, 0]} />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Candidates by Source */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">Candidatos por Fuente</CardTitle>
|
|
<CardDescription>Distribucion de fuentes de reclutamiento</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex items-center justify-center">
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<RechartsPieChart>
|
|
<Pie
|
|
data={candidatesBySource}
|
|
cx="50%"
|
|
cy="50%"
|
|
innerRadius={60}
|
|
outerRadius={100}
|
|
paddingAngle={2}
|
|
dataKey="value"
|
|
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
|
|
>
|
|
{candidatesBySource.map((entry, index) => (
|
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
|
))}
|
|
</Pie>
|
|
<Tooltip />
|
|
</RechartsPieChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
<div className="flex flex-wrap justify-center gap-4 mt-4">
|
|
{candidatesBySource.map((source) => (
|
|
<div key={source.name} className="flex items-center gap-2">
|
|
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: source.color }} />
|
|
<span className="text-sm">{source.name}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{/* Time to Hire */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">Tiempo Promedio de Contratacion</CardTitle>
|
|
<CardDescription>Dias promedio desde aplicacion hasta contratacion</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ResponsiveContainer width="100%" height={250}>
|
|
<AreaChart data={timeToHire}>
|
|
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
|
<XAxis dataKey="month" className="text-xs" />
|
|
<YAxis className="text-xs" />
|
|
<Tooltip
|
|
contentStyle={{
|
|
backgroundColor: "hsl(var(--card))",
|
|
border: "1px solid hsl(var(--border))",
|
|
borderRadius: "8px"
|
|
}}
|
|
/>
|
|
<Area
|
|
type="monotone"
|
|
dataKey="dias"
|
|
stroke="#6ec1e4"
|
|
fill="#6ec1e4"
|
|
fillOpacity={0.3}
|
|
name="Dias"
|
|
/>
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Requisitions by Department */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">Requisiciones por Departamento</CardTitle>
|
|
<CardDescription>Vacantes abiertas vs cubiertas por area</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ResponsiveContainer width="100%" height={250}>
|
|
<BarChart data={requisitionsByDepartment} layout="vertical">
|
|
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
|
<XAxis type="number" className="text-xs" />
|
|
<YAxis dataKey="name" type="category" className="text-xs" width={80} />
|
|
<Tooltip
|
|
contentStyle={{
|
|
backgroundColor: "hsl(var(--card))",
|
|
border: "1px solid hsl(var(--border))",
|
|
borderRadius: "8px"
|
|
}}
|
|
/>
|
|
<Legend />
|
|
<Bar dataKey="abiertas" fill="#6ec1e4" name="Abiertas" radius={[0, 4, 4, 0]} />
|
|
<Bar dataKey="cubiertas" fill="#61ce70" name="Cubiertas" radius={[0, 4, 4, 0]} />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="requisitions" className="space-y-6">
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<Card className="lg:col-span-2">
|
|
<CardHeader>
|
|
<CardTitle className="text-base">Estado de Requisiciones</CardTitle>
|
|
<CardDescription>Distribucion por estado actual</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
{[
|
|
{ status: "Borrador", count: 3, color: "bg-muted" },
|
|
{ status: "Pendiente Aprobacion", count: 5, color: "bg-yellow-500" },
|
|
{ status: "Aprobada", count: 8, color: "bg-blue-500" },
|
|
{ status: "Publicada", count: 12, color: "bg-primary" },
|
|
{ status: "En Proceso", count: 15, color: "bg-purple-500" },
|
|
{ status: "Cubierta", count: 25, color: "bg-accent" },
|
|
{ status: "Cancelada", count: 2, color: "bg-destructive" }
|
|
].map(item => (
|
|
<div key={item.status} className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm font-medium">{item.status}</span>
|
|
<span className="text-sm text-muted-foreground">{item.count}</span>
|
|
</div>
|
|
<Progress
|
|
value={(item.count / 70) * 100}
|
|
className="h-2"
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">Prioridad de Vacantes</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between p-3 bg-destructive/10 rounded-lg">
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-2 h-2 rounded-full bg-destructive" />
|
|
<span className="text-sm font-medium">Urgente</span>
|
|
</div>
|
|
<span className="font-bold">5</span>
|
|
</div>
|
|
<div className="flex items-center justify-between p-3 bg-yellow-500/10 rounded-lg">
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-2 h-2 rounded-full bg-yellow-500" />
|
|
<span className="text-sm font-medium">Alta</span>
|
|
</div>
|
|
<span className="font-bold">12</span>
|
|
</div>
|
|
<div className="flex items-center justify-between p-3 bg-primary/10 rounded-lg">
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-2 h-2 rounded-full bg-primary" />
|
|
<span className="text-sm font-medium">Media</span>
|
|
</div>
|
|
<span className="font-bold">18</span>
|
|
</div>
|
|
<div className="flex items-center justify-between p-3 bg-muted rounded-lg">
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-2 h-2 rounded-full bg-muted-foreground" />
|
|
<span className="text-sm font-medium">Baja</span>
|
|
</div>
|
|
<span className="font-bold">8</span>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="candidates" className="space-y-6">
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">Candidatos por Etapa</CardTitle>
|
|
<CardDescription>Distribucion actual en el pipeline</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<BarChart data={pipelineConversion}>
|
|
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
|
<XAxis dataKey="stage" className="text-xs" angle={-45} textAnchor="end" height={80} />
|
|
<YAxis className="text-xs" />
|
|
<Tooltip
|
|
contentStyle={{
|
|
backgroundColor: "hsl(var(--card))",
|
|
border: "1px solid hsl(var(--border))",
|
|
borderRadius: "8px"
|
|
}}
|
|
/>
|
|
<Bar dataKey="count" fill="#6ec1e4" name="Candidatos" radius={[4, 4, 0, 0]} />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">Metricas de Candidatos</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm">Tasa de Respuesta</span>
|
|
<span className="font-bold">78%</span>
|
|
</div>
|
|
<Progress value={78} className="h-2" />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm">Tasa de Entrevista</span>
|
|
<span className="font-bold">45%</span>
|
|
</div>
|
|
<Progress value={45} className="h-2" />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm">Tasa de Oferta</span>
|
|
<span className="font-bold">12%</span>
|
|
</div>
|
|
<Progress value={12} className="h-2" />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm">Tasa de Aceptacion</span>
|
|
<span className="font-bold">85%</span>
|
|
</div>
|
|
<Progress value={85} className="h-2" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="pipeline" className="space-y-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">Funnel de Conversion</CardTitle>
|
|
<CardDescription>Conversion de candidatos a traves del pipeline</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
{pipelineConversion.map((stage, index) => (
|
|
<div key={stage.stage} className="relative">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-24 text-sm font-medium">{stage.stage}</div>
|
|
<div className="flex-1">
|
|
<div
|
|
className="h-10 bg-primary/20 rounded-lg flex items-center px-4 transition-all"
|
|
style={{ width: `${stage.percentage}%`, minWidth: "80px" }}
|
|
>
|
|
<span className="font-bold text-primary">{stage.count}</span>
|
|
</div>
|
|
</div>
|
|
<div className="w-16 text-right text-sm text-muted-foreground">
|
|
{stage.percentage}%
|
|
</div>
|
|
</div>
|
|
{index < pipelineConversion.length - 1 && (
|
|
<div className="ml-12 pl-4 border-l-2 border-dashed border-muted h-4" />
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
)
|
|
}
|