Initial commit: Proyecto portal-requisicion-vacantes
This commit is contained in:
@@ -0,0 +1,706 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import {
|
||||
ArrowLeft,
|
||||
Edit,
|
||||
Clock,
|
||||
Users,
|
||||
MapPin,
|
||||
Building2,
|
||||
Calendar,
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Send,
|
||||
RefreshCw,
|
||||
UserPlus,
|
||||
FileText,
|
||||
History,
|
||||
MessageSquare,
|
||||
Briefcase,
|
||||
DollarSign,
|
||||
Target,
|
||||
ChevronRight,
|
||||
Ban,
|
||||
} from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { requisitions, auditLog, users, recruiterCapacity } from '@/lib/mock-data'
|
||||
import {
|
||||
STATUS_CONFIG,
|
||||
URGENCY_CONFIG,
|
||||
STAGE_CONFIG,
|
||||
type Requisition,
|
||||
type CandidateStage
|
||||
} from '@/lib/types'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
function getInitials(name: string): string {
|
||||
return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
|
||||
}
|
||||
|
||||
function getDaysUntilDeadline(deadline: string): number {
|
||||
const now = new Date()
|
||||
const deadlineDate = new Date(deadline)
|
||||
return Math.ceil((deadlineDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
|
||||
}
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
return new Date(dateString).toLocaleDateString('es-ES', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
function formatDateTime(dateString: string): string {
|
||||
return new Date(dateString).toLocaleString('es-ES', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
// Status flow visualization component
|
||||
function StatusFlow({ currentStatus }: { currentStatus: Requisition['status'] }) {
|
||||
const steps = [
|
||||
{ key: 'borrador', label: 'Borrador', icon: FileText },
|
||||
{ key: 'pendiente_rrhh', label: 'RR.HH.', icon: Users },
|
||||
{ key: 'pendiente_admin', label: 'Admin', icon: Building2 },
|
||||
{ key: 'reclutando', label: 'Reclutando', icon: Briefcase },
|
||||
{ key: 'cerrado', label: 'Cerrado', icon: CheckCircle2 },
|
||||
]
|
||||
|
||||
const currentIndex = steps.findIndex(s => s.key === currentStatus)
|
||||
const isCancelled = currentStatus === 'cancelado'
|
||||
const isCorrection = currentStatus === 'correccion_requerida'
|
||||
|
||||
return (
|
||||
<div className="relative overflow-x-auto pb-4">
|
||||
<div className="flex items-center justify-between min-w-[500px]">
|
||||
{steps.map((step, index) => {
|
||||
const Icon = step.icon
|
||||
const isCompleted = index < currentIndex && !isCancelled
|
||||
const isCurrent = step.key === currentStatus
|
||||
const isPending = index > currentIndex
|
||||
|
||||
return (
|
||||
<React.Fragment key={step.key}>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className={cn(
|
||||
'flex h-10 w-10 items-center justify-center rounded-full border-2 transition-colors',
|
||||
isCompleted && 'border-emerald-500 bg-emerald-500 text-white',
|
||||
isCurrent && !isCancelled && 'border-primary bg-primary text-primary-foreground',
|
||||
isPending && 'border-muted-foreground/30 bg-muted text-muted-foreground',
|
||||
isCancelled && 'border-red-500 bg-red-500 text-white',
|
||||
isCorrection && isCurrent && 'border-amber-500 bg-amber-500 text-white'
|
||||
)}>
|
||||
{isCompleted ? (
|
||||
<CheckCircle2 className="h-5 w-5" />
|
||||
) : isCancelled ? (
|
||||
<XCircle className="h-5 w-5" />
|
||||
) : (
|
||||
<Icon className="h-5 w-5" />
|
||||
)}
|
||||
</div>
|
||||
<span className={cn(
|
||||
'text-xs font-medium',
|
||||
(isCompleted || isCurrent) && 'text-foreground',
|
||||
isPending && 'text-muted-foreground'
|
||||
)}>
|
||||
{step.label}
|
||||
</span>
|
||||
</div>
|
||||
{index < steps.length - 1 && (
|
||||
<div className={cn(
|
||||
'flex-1 h-0.5 mx-2 -mt-6',
|
||||
index < currentIndex ? 'bg-emerald-500' : 'bg-muted-foreground/30'
|
||||
)} />
|
||||
)}
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{isCorrection && (
|
||||
<div className="mt-4 flex items-center justify-center gap-2 text-amber-600">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<span className="text-sm font-medium">Corrección requerida - Regresado a solicitante</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Recruitment stages component
|
||||
function RecruitmentStages({ currentStage }: { currentStage?: CandidateStage }) {
|
||||
if (!currentStage) return null
|
||||
|
||||
const stages: CandidateStage[] = ['reclutando', 'primera_entrevista', 'segunda_entrevista', 'referencias', 'examenes_medicos', 'seleccionado']
|
||||
const currentIndex = stages.indexOf(currentStage)
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{stages.map((stage, index) => {
|
||||
const config = STAGE_CONFIG[stage]
|
||||
const isCompleted = index < currentIndex
|
||||
const isCurrent = stage === currentStage
|
||||
|
||||
return (
|
||||
<div key={stage} className="flex items-center gap-3">
|
||||
<div className={cn(
|
||||
'flex h-8 w-8 items-center justify-center rounded-full text-xs font-medium',
|
||||
isCompleted && 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400',
|
||||
isCurrent && 'bg-primary text-primary-foreground',
|
||||
!isCompleted && !isCurrent && 'bg-muted text-muted-foreground'
|
||||
)}>
|
||||
{isCompleted ? <CheckCircle2 className="h-4 w-4" /> : index + 1}
|
||||
</div>
|
||||
<span className={cn(
|
||||
'text-sm',
|
||||
isCurrent && 'font-medium text-foreground',
|
||||
isCompleted && 'text-muted-foreground line-through',
|
||||
!isCompleted && !isCurrent && 'text-muted-foreground'
|
||||
)}>
|
||||
{config.label}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface RequisitionDetailPageProps {
|
||||
requisitionId: string
|
||||
}
|
||||
|
||||
export function RequisitionDetailPage({ requisitionId }: RequisitionDetailPageProps) {
|
||||
const router = useRouter()
|
||||
const [cancelDialogOpen, setCancelDialogOpen] = React.useState(false)
|
||||
const [assignDialogOpen, setAssignDialogOpen] = React.useState(false)
|
||||
const [cancelReason, setCancelReason] = React.useState('')
|
||||
const [selectedRecruiter, setSelectedRecruiter] = React.useState('')
|
||||
|
||||
const requisition = requisitions.find(r => r.id === requisitionId)
|
||||
|
||||
if (!requisition) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-[60vh]">
|
||||
<AlertTriangle className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<h2 className="text-lg font-semibold">Requisición no encontrada</h2>
|
||||
<p className="text-muted-foreground mb-4">La requisición {requisitionId} no existe.</p>
|
||||
<Button asChild>
|
||||
<Link href="/requisiciones">Volver a requisiciones</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const status = STATUS_CONFIG[requisition.status]
|
||||
const urgency = URGENCY_CONFIG[requisition.urgency]
|
||||
const daysLeft = getDaysUntilDeadline(requisition.hardDeadline)
|
||||
const isOverdue = daysLeft < 0
|
||||
const isAtRisk = daysLeft >= 0 && daysLeft <= 7
|
||||
const progress = (requisition.vacanciesFilled / requisition.quantity) * 100
|
||||
|
||||
const relatedAuditLog = auditLog.filter(log => log.entityId === requisitionId)
|
||||
const recruiters = users.filter(u => u.role === 'reclutador' && u.active)
|
||||
|
||||
const handleCancel = () => {
|
||||
toast.success('Requisición cancelada', {
|
||||
description: `${requisition.id} ha sido cancelada.`,
|
||||
})
|
||||
setCancelDialogOpen(false)
|
||||
router.push('/requisiciones')
|
||||
}
|
||||
|
||||
const handleAssignRecruiter = () => {
|
||||
const recruiter = recruiters.find(r => r.id === selectedRecruiter)
|
||||
toast.success('Reclutador asignado', {
|
||||
description: `${recruiter?.name} ha sido asignado a ${requisition.id}.`,
|
||||
})
|
||||
setAssignDialogOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="flex items-start gap-4">
|
||||
<Button variant="ghost" size="icon" asChild>
|
||||
<Link href="/requisiciones">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<h1 className="text-2xl font-bold tracking-tight">{requisition.id}</h1>
|
||||
<Badge variant="outline" className={cn(status.color, status.bgColor, status.borderColor)}>
|
||||
{status.label}
|
||||
</Badge>
|
||||
<Badge variant="outline" className={cn('text-xs', urgency.color, urgency.bgColor)}>
|
||||
{urgency.label}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
{requisition.positionName} - {requisition.quantity} vacante{requisition.quantity > 1 ? 's' : ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{requisition.status === 'borrador' && (
|
||||
<>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href={`/requisiciones/${requisition.id}/editar`}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Editar
|
||||
</Link>
|
||||
</Button>
|
||||
<Button>
|
||||
<Send className="mr-2 h-4 w-4" />
|
||||
Enviar a Aprobación
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{requisition.status === 'correccion_requerida' && (
|
||||
<Button asChild>
|
||||
<Link href={`/requisiciones/${requisition.id}/editar`}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Corregir y Reenviar
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
{requisition.status === 'reclutando' && !requisition.recruiterId && (
|
||||
<Dialog open={assignDialogOpen} onOpenChange={setAssignDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<UserPlus className="mr-2 h-4 w-4" />
|
||||
Asignar Reclutador
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Asignar Reclutador</DialogTitle>
|
||||
<DialogDescription>
|
||||
Selecciona el reclutador que gestionará esta requisición.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Reclutador</Label>
|
||||
<Select value={selectedRecruiter} onValueChange={setSelectedRecruiter}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Seleccionar reclutador" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{recruiters.map((recruiter) => {
|
||||
const capacity = recruiterCapacity.find(c => c.recruiterId === recruiter.id)
|
||||
return (
|
||||
<SelectItem key={recruiter.id} value={recruiter.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{recruiter.name}</span>
|
||||
{capacity && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
(Score: {capacity.currentScore}/{capacity.maxThreshold})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
)
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setAssignDialogOpen(false)}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button onClick={handleAssignRecruiter} disabled={!selectedRecruiter}>
|
||||
Asignar
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
{!['cerrado', 'cancelado'].includes(requisition.status) && (
|
||||
<Dialog open={cancelDialogOpen} onOpenChange={setCancelDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" className="text-destructive hover:text-destructive">
|
||||
<Ban className="mr-2 h-4 w-4" />
|
||||
Cancelar
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Cancelar Requisición</DialogTitle>
|
||||
<DialogDescription>
|
||||
Esta acción no se puede deshacer. La requisición quedará marcada como cancelada.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="cancelReason">Motivo de cancelación *</Label>
|
||||
<Textarea
|
||||
id="cancelReason"
|
||||
value={cancelReason}
|
||||
onChange={(e) => setCancelReason(e.target.value)}
|
||||
placeholder="Describe el motivo de la cancelación..."
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setCancelDialogOpen(false)}>
|
||||
Volver
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleCancel} disabled={!cancelReason.trim()}>
|
||||
Confirmar Cancelación
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Flow */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Estado del Proceso</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<StatusFlow currentStatus={requisition.status} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
{/* Left Column - Details */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Rejection Comment */}
|
||||
{requisition.rejectionComment && (
|
||||
<Card className="border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950/30">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertTriangle className="h-5 w-5 text-amber-600 shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-medium text-amber-800 dark:text-amber-200">Corrección Requerida</p>
|
||||
<p className="text-sm text-amber-700 dark:text-amber-300 mt-1">
|
||||
{requisition.rejectionComment}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Info Tabs */}
|
||||
<Tabs defaultValue="detalles">
|
||||
<TabsList>
|
||||
<TabsTrigger value="detalles">Detalles</TabsTrigger>
|
||||
<TabsTrigger value="historial">Historial</TabsTrigger>
|
||||
{requisition.status === 'reclutando' && (
|
||||
<TabsTrigger value="pipeline">Pipeline</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="detalles" className="mt-4">
|
||||
<Card>
|
||||
<CardContent className="p-6 space-y-6">
|
||||
{/* Basic Info */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-4">
|
||||
Información de la Vacante
|
||||
</h3>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">Posición</p>
|
||||
<p className="font-medium">{requisition.positionName}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">Departamento</p>
|
||||
<p className="font-medium">{requisition.departmentName}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">Ubicación</p>
|
||||
<p className="font-medium">{requisition.cityName}, {requisition.countryName}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">Modalidad</p>
|
||||
<p className="font-medium capitalize">{requisition.modalidad}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">Tipo de Contratación</p>
|
||||
<p className="font-medium capitalize">{requisition.tipoContratacion}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">Cantidad de Vacantes</p>
|
||||
<p className="font-medium">{requisition.quantity}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Justification */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-4">
|
||||
Justificación
|
||||
</h3>
|
||||
<p className="text-sm leading-relaxed">{requisition.justification}</p>
|
||||
</div>
|
||||
|
||||
{/* Compensation (if visible) */}
|
||||
{(requisition.salaryPackage || requisition.budget) && (
|
||||
<>
|
||||
<Separator />
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-4">
|
||||
Compensación
|
||||
</h3>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{requisition.salaryPackage && (
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">Paquete Salarial</p>
|
||||
<p className="font-medium">{requisition.salaryPackage}</p>
|
||||
</div>
|
||||
)}
|
||||
{requisition.budget && (
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">Presupuesto</p>
|
||||
<p className="font-medium">
|
||||
{requisition.currency} {requisition.budget.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="historial" className="mt-4">
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<ScrollArea className="h-[400px] pr-4">
|
||||
<div className="relative pl-6 border-l-2 border-muted space-y-6">
|
||||
{relatedAuditLog.map((log, index) => (
|
||||
<div key={log.id} className="relative">
|
||||
<div className="absolute -left-[25px] h-3 w-3 rounded-full bg-primary" />
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="font-medium text-sm">{log.action}</p>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatDateTime(log.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">Por {log.userName}</p>
|
||||
{log.oldValue && log.newValue && (
|
||||
<div className="flex items-center gap-2 text-xs mt-1">
|
||||
<Badge variant="outline" className="text-muted-foreground">
|
||||
{log.oldValue}
|
||||
</Badge>
|
||||
<ChevronRight className="h-3 w-3 text-muted-foreground" />
|
||||
<Badge variant="outline">
|
||||
{log.newValue}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{requisition.status === 'reclutando' && (
|
||||
<TabsContent value="pipeline" className="mt-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Etapas de Reclutamiento</CardTitle>
|
||||
<CardDescription>Progreso del proceso de selección</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<RecruitmentStages currentStage={requisition.currentStage} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Summary */}
|
||||
<div className="space-y-6">
|
||||
{/* SLA Card */}
|
||||
<Card className={cn(
|
||||
isOverdue && 'border-red-200 dark:border-red-800',
|
||||
isAtRisk && !isOverdue && 'border-amber-200 dark:border-amber-800'
|
||||
)}>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Target className="h-4 w-4" />
|
||||
SLA
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">Fecha límite</span>
|
||||
<span className="font-medium">{formatDate(requisition.hardDeadline)}</span>
|
||||
</div>
|
||||
<div className={cn(
|
||||
'flex items-center justify-between',
|
||||
isOverdue && 'text-red-600 dark:text-red-400',
|
||||
isAtRisk && !isOverdue && 'text-amber-600 dark:text-amber-400'
|
||||
)}>
|
||||
<span className="text-sm">Días restantes</span>
|
||||
<span className="font-semibold text-lg">
|
||||
{isOverdue ? `${Math.abs(daysLeft)} días vencido` : `${daysLeft} días`}
|
||||
</span>
|
||||
</div>
|
||||
{(isOverdue || isAtRisk) && (
|
||||
<div className={cn(
|
||||
'flex items-center gap-2 rounded-lg p-3 text-sm',
|
||||
isOverdue && 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300',
|
||||
isAtRisk && !isOverdue && 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
|
||||
)}>
|
||||
<AlertTriangle className="h-4 w-4 shrink-0" />
|
||||
<span>
|
||||
{isOverdue ? 'SLA vencido - Acción inmediata requerida' : 'SLA en riesgo - Priorizar'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Progress Card */}
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Users className="h-4 w-4" />
|
||||
Progreso de Vacantes
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-end justify-between">
|
||||
<div>
|
||||
<span className="text-3xl font-bold">{requisition.vacanciesFilled}</span>
|
||||
<span className="text-muted-foreground"> / {requisition.quantity}</span>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{Math.round(progress)}% completado
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={progress} className="h-2" />
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{requisition.quantity - requisition.vacanciesFilled} vacante{(requisition.quantity - requisition.vacanciesFilled) !== 1 ? 's' : ''} pendiente{(requisition.quantity - requisition.vacanciesFilled) !== 1 ? 's' : ''}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Requester Card */}
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base">Solicitante</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar>
|
||||
<AvatarFallback>{getInitials(requisition.requesterName)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<p className="font-medium">{requisition.requesterName}</p>
|
||||
<p className="text-sm text-muted-foreground">{requisition.departmentName}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Fecha de creación</span>
|
||||
<span>{formatDate(requisition.createdAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Recruiter Card */}
|
||||
{requisition.recruiterId && (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base">Reclutador Asignado</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar>
|
||||
<AvatarFallback>{getInitials(requisition.recruiterName || '')}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<p className="font-medium">{requisition.recruiterName}</p>
|
||||
<p className="text-sm text-muted-foreground">Reclutador</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Approvals Card */}
|
||||
{(requisition.approvedByHrAt || requisition.approvedByAdminAt) && (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base">Aprobaciones</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{requisition.approvedByHrAt && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<CheckCircle2 className="h-4 w-4 text-emerald-500" />
|
||||
<span>RR.HH. - {formatDate(requisition.approvedByHrAt)}</span>
|
||||
</div>
|
||||
)}
|
||||
{requisition.approvedByAdminAt && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<CheckCircle2 className="h-4 w-4 text-emerald-500" />
|
||||
<span>Administración - {formatDate(requisition.approvedByAdminAt)}</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user