Files
2026-06-12 16:08:49 -04:00

706 lines
29 KiB
TypeScript

'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 { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { Separator } from '@/components/ui/separator'
import {
ArrowLeft,
ArrowRight,
Save,
Send,
MapPin,
Building2,
Briefcase,
FileText,
DollarSign,
CheckCircle2,
AlertCircle,
} from 'lucide-react'
import { cn } from '@/lib/utils'
import { countries, cities, departments, positions, currentUser, getCitiesByCountry } from '@/lib/mock-data'
import { toast } from 'sonner'
type FormStep = 'ubicacion' | 'vacante' | 'justificacion' | 'compensacion' | 'revision'
const STEPS: { key: FormStep; label: string; icon: React.ElementType }[] = [
{ key: 'ubicacion', label: 'Ubicación', icon: MapPin },
{ key: 'vacante', label: 'Vacante', icon: Briefcase },
{ key: 'justificacion', label: 'Justificación', icon: FileText },
{ key: 'compensacion', label: 'Compensación', icon: DollarSign },
{ key: 'revision', label: 'Revisión', icon: CheckCircle2 },
]
interface FormData {
countryId: string
cityId: string
departmentId: string
positionId: string
quantity: number
modalidad: 'presencial' | 'remoto' | 'hibrido'
tipoContratacion: 'nuevo' | 'reemplazo' | 'temporal'
justification: string
urgency: 'normal' | 'alta' | 'critica'
salaryPackage: string
budget: string
currency: string
}
function StepIndicator({ currentStep }: { currentStep: FormStep }) {
const currentIndex = STEPS.findIndex(s => s.key === currentStep)
return (
<div className="flex items-center justify-between mb-8">
{STEPS.map((step, index) => {
const Icon = step.icon
const isCompleted = index < currentIndex
const isCurrent = step.key === currentStep
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 && 'border-primary bg-primary text-primary-foreground',
!isCompleted && !isCurrent && 'border-muted-foreground/30 bg-muted text-muted-foreground'
)}>
{isCompleted ? (
<CheckCircle2 className="h-5 w-5" />
) : (
<Icon className="h-5 w-5" />
)}
</div>
<span className={cn(
'text-xs font-medium hidden sm:block',
(isCompleted || isCurrent) && 'text-foreground',
!isCompleted && !isCurrent && 'text-muted-foreground'
)}>
{step.label}
</span>
</div>
{index < STEPS.length - 1 && (
<div className={cn(
'flex-1 h-0.5 mx-2',
index < currentIndex ? 'bg-emerald-500' : 'bg-muted-foreground/30'
)} />
)}
</React.Fragment>
)
})}
</div>
)
}
export function NewRequisitionPage() {
const router = useRouter()
const [currentStep, setCurrentStep] = React.useState<FormStep>('ubicacion')
const [errors, setErrors] = React.useState<Partial<Record<keyof FormData, string>>>({})
const [formData, setFormData] = React.useState<FormData>({
countryId: '',
cityId: '',
departmentId: '',
positionId: '',
quantity: 1,
modalidad: 'presencial',
tipoContratacion: 'nuevo',
justification: '',
urgency: 'normal',
salaryPackage: '',
budget: '',
currency: 'USD',
})
const availableCities = formData.countryId ? getCitiesByCountry(formData.countryId) : []
const updateFormData = (field: keyof FormData, value: string | number) => {
setFormData(prev => ({ ...prev, [field]: value }))
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: undefined }))
}
// Reset city when country changes
if (field === 'countryId') {
setFormData(prev => ({ ...prev, cityId: '' }))
}
}
const validateStep = (step: FormStep): boolean => {
const newErrors: Partial<Record<keyof FormData, string>> = {}
if (step === 'ubicacion') {
if (!formData.countryId) newErrors.countryId = 'Selecciona un país'
if (!formData.cityId) newErrors.cityId = 'Selecciona una ciudad'
if (!formData.departmentId) newErrors.departmentId = 'Selecciona un departamento'
}
if (step === 'vacante') {
if (!formData.positionId) newErrors.positionId = 'Selecciona una posición'
if (formData.quantity < 1) newErrors.quantity = 'La cantidad debe ser al menos 1'
}
if (step === 'justificacion') {
if (!formData.justification.trim()) newErrors.justification = 'La justificación es obligatoria'
if (formData.justification.length < 20) newErrors.justification = 'Proporciona una justificación más detallada (mín. 20 caracteres)'
}
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const goToNextStep = () => {
if (!validateStep(currentStep)) return
const currentIndex = STEPS.findIndex(s => s.key === currentStep)
if (currentIndex < STEPS.length - 1) {
setCurrentStep(STEPS[currentIndex + 1].key)
}
}
const goToPreviousStep = () => {
const currentIndex = STEPS.findIndex(s => s.key === currentStep)
if (currentIndex > 0) {
setCurrentStep(STEPS[currentIndex - 1].key)
}
}
const handleSaveDraft = () => {
toast.success('Borrador guardado', {
description: 'La requisición se ha guardado como borrador.',
})
}
const handleSubmit = () => {
toast.success('Requisición enviada', {
description: 'La requisición ha sido enviada para aprobación de RR.HH.',
})
router.push('/requisiciones')
}
const selectedCountry = countries.find(c => c.id === formData.countryId)
const selectedCity = cities.find(c => c.id === formData.cityId)
const selectedDepartment = departments.find(d => d.id === formData.departmentId)
const selectedPosition = positions.find(p => p.id === formData.positionId)
return (
<div className="p-6 max-w-4xl mx-auto">
{/* Header */}
<div className="flex items-center gap-4 mb-6">
<Button variant="ghost" size="icon" asChild>
<Link href="/requisiciones">
<ArrowLeft className="h-4 w-4" />
</Link>
</Button>
<div>
<h1 className="text-2xl font-bold tracking-tight">Nueva Requisición</h1>
<p className="text-muted-foreground">
Completa el formulario para solicitar una nueva vacante
</p>
</div>
</div>
{/* Step Indicator */}
<StepIndicator currentStep={currentStep} />
{/* Form Card */}
<Card>
<CardContent className="p-6">
{/* Step 1: Ubicación */}
{currentStep === 'ubicacion' && (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold mb-1">Ubicación</h2>
<p className="text-sm text-muted-foreground">
Define el país, ciudad y departamento de la vacante
</p>
</div>
<div className="grid gap-6 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="country">País *</Label>
<Select value={formData.countryId} onValueChange={(v) => updateFormData('countryId', v)}>
<SelectTrigger className={cn(errors.countryId && 'border-destructive')}>
<SelectValue placeholder="Seleccionar país" />
</SelectTrigger>
<SelectContent>
{countries.map((country) => (
<SelectItem key={country.id} value={country.id}>
{country.name}
</SelectItem>
))}
</SelectContent>
</Select>
{errors.countryId && (
<p className="text-xs text-destructive flex items-center gap-1">
<AlertCircle className="h-3 w-3" />
{errors.countryId}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="city">Ciudad *</Label>
<Select
value={formData.cityId}
onValueChange={(v) => updateFormData('cityId', v)}
disabled={!formData.countryId}
>
<SelectTrigger className={cn(errors.cityId && 'border-destructive')}>
<SelectValue placeholder={formData.countryId ? "Seleccionar ciudad" : "Primero selecciona un país"} />
</SelectTrigger>
<SelectContent>
{availableCities.map((city) => (
<SelectItem key={city.id} value={city.id}>
{city.name}
</SelectItem>
))}
</SelectContent>
</Select>
{errors.cityId && (
<p className="text-xs text-destructive flex items-center gap-1">
<AlertCircle className="h-3 w-3" />
{errors.cityId}
</p>
)}
</div>
<div className="space-y-2 sm:col-span-2">
<Label htmlFor="department">Departamento *</Label>
<Select value={formData.departmentId} onValueChange={(v) => updateFormData('departmentId', v)}>
<SelectTrigger className={cn(errors.departmentId && 'border-destructive')}>
<SelectValue placeholder="Seleccionar departamento" />
</SelectTrigger>
<SelectContent>
{departments.map((dept) => (
<SelectItem key={dept.id} value={dept.id}>
{dept.name}
</SelectItem>
))}
</SelectContent>
</Select>
{errors.departmentId && (
<p className="text-xs text-destructive flex items-center gap-1">
<AlertCircle className="h-3 w-3" />
{errors.departmentId}
</p>
)}
</div>
</div>
{/* Requester Info */}
<div className="rounded-lg bg-muted/50 p-4">
<p className="text-sm text-muted-foreground mb-2">Solicitante</p>
<p className="font-medium">{currentUser.name}</p>
<p className="text-sm text-muted-foreground">{currentUser.email}</p>
</div>
</div>
)}
{/* Step 2: Vacante */}
{currentStep === 'vacante' && (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold mb-1">Información de la Vacante</h2>
<p className="text-sm text-muted-foreground">
Define la posición, cantidad y tipo de contratación
</p>
</div>
<div className="grid gap-6 sm:grid-cols-2">
<div className="space-y-2 sm:col-span-2">
<Label htmlFor="position">Posición *</Label>
<Select value={formData.positionId} onValueChange={(v) => updateFormData('positionId', v)}>
<SelectTrigger className={cn(errors.positionId && 'border-destructive')}>
<SelectValue placeholder="Seleccionar posición" />
</SelectTrigger>
<SelectContent>
{positions.map((pos) => (
<SelectItem key={pos.id} value={pos.id}>
{pos.name}
</SelectItem>
))}
</SelectContent>
</Select>
{errors.positionId && (
<p className="text-xs text-destructive flex items-center gap-1">
<AlertCircle className="h-3 w-3" />
{errors.positionId}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="quantity">Cantidad de Vacantes *</Label>
<Input
id="quantity"
type="number"
min={1}
value={formData.quantity}
onChange={(e) => updateFormData('quantity', parseInt(e.target.value) || 1)}
className={cn(errors.quantity && 'border-destructive')}
/>
{errors.quantity && (
<p className="text-xs text-destructive flex items-center gap-1">
<AlertCircle className="h-3 w-3" />
{errors.quantity}
</p>
)}
</div>
<div className="space-y-2">
<Label>Modalidad *</Label>
<RadioGroup
value={formData.modalidad}
onValueChange={(v) => updateFormData('modalidad', v)}
className="flex gap-4"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="presencial" id="presencial" />
<Label htmlFor="presencial" className="font-normal">Presencial</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="hibrido" id="hibrido" />
<Label htmlFor="hibrido" className="font-normal">Híbrido</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="remoto" id="remoto" />
<Label htmlFor="remoto" className="font-normal">Remoto</Label>
</div>
</RadioGroup>
</div>
<div className="space-y-2 sm:col-span-2">
<Label>Tipo de Contratación *</Label>
<RadioGroup
value={formData.tipoContratacion}
onValueChange={(v) => updateFormData('tipoContratacion', v)}
className="flex gap-4"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="nuevo" id="nuevo" />
<Label htmlFor="nuevo" className="font-normal">Nueva posición</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="reemplazo" id="reemplazo" />
<Label htmlFor="reemplazo" className="font-normal">Reemplazo</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="temporal" id="temporal" />
<Label htmlFor="temporal" className="font-normal">Temporal</Label>
</div>
</RadioGroup>
</div>
</div>
</div>
)}
{/* Step 3: Justificación */}
{currentStep === 'justificacion' && (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold mb-1">Justificación</h2>
<p className="text-sm text-muted-foreground">
Explica el motivo de la requisición y su nivel de urgencia
</p>
</div>
<div className="space-y-6">
<div className="space-y-2">
<Label htmlFor="justification">Justificación de la Requisición *</Label>
<Textarea
id="justification"
value={formData.justification}
onChange={(e) => updateFormData('justification', e.target.value)}
placeholder="Describe el motivo por el cual se necesita esta contratación, el impacto en el negocio y cualquier contexto relevante..."
rows={5}
className={cn(errors.justification && 'border-destructive')}
/>
<div className="flex items-center justify-between">
{errors.justification ? (
<p className="text-xs text-destructive flex items-center gap-1">
<AlertCircle className="h-3 w-3" />
{errors.justification}
</p>
) : (
<p className="text-xs text-muted-foreground">
Mínimo 20 caracteres
</p>
)}
<p className="text-xs text-muted-foreground">
{formData.justification.length} caracteres
</p>
</div>
</div>
<div className="space-y-2">
<Label>Nivel de Urgencia *</Label>
<RadioGroup
value={formData.urgency}
onValueChange={(v) => updateFormData('urgency', v)}
className="grid gap-3"
>
<div className="flex items-start space-x-3 rounded-lg border p-4 hover:bg-muted/50 transition-colors">
<RadioGroupItem value="normal" id="normal" className="mt-0.5" />
<div>
<Label htmlFor="normal" className="font-medium">Normal</Label>
<p className="text-sm text-muted-foreground">
Proceso estándar de reclutamiento según SLA definido
</p>
</div>
</div>
<div className="flex items-start space-x-3 rounded-lg border border-amber-200 bg-amber-50/50 p-4 dark:border-amber-800 dark:bg-amber-950/30">
<RadioGroupItem value="alta" id="alta" className="mt-0.5" />
<div>
<Label htmlFor="alta" className="font-medium">Alta</Label>
<p className="text-sm text-muted-foreground">
Requiere atención prioritaria, impacto operativo significativo
</p>
</div>
</div>
<div className="flex items-start space-x-3 rounded-lg border border-red-200 bg-red-50/50 p-4 dark:border-red-800 dark:bg-red-950/30">
<RadioGroupItem value="critica" id="critica" className="mt-0.5" />
<div>
<Label htmlFor="critica" className="font-medium">Crítica</Label>
<p className="text-sm text-muted-foreground">
Urgencia máxima, afecta operaciones críticas del negocio
</p>
</div>
</div>
</RadioGroup>
</div>
</div>
</div>
)}
{/* Step 4: Compensación */}
{currentStep === 'compensacion' && (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold mb-1">Compensación</h2>
<p className="text-sm text-muted-foreground">
Define el paquete salarial y presupuesto (opcional, visible solo para roles autorizados)
</p>
</div>
<div className="grid gap-6 sm:grid-cols-2">
<div className="space-y-2 sm:col-span-2">
<Label htmlFor="salaryPackage">Paquete Salarial</Label>
<Input
id="salaryPackage"
value={formData.salaryPackage}
onChange={(e) => updateFormData('salaryPackage', e.target.value)}
placeholder="Ej: $15,000 - $20,000 MXN mensual"
/>
<p className="text-xs text-muted-foreground">
Rango salarial propuesto para la posición
</p>
</div>
<div className="space-y-2">
<Label htmlFor="budget">Presupuesto Mensual</Label>
<Input
id="budget"
type="number"
value={formData.budget}
onChange={(e) => updateFormData('budget', e.target.value)}
placeholder="0"
/>
</div>
<div className="space-y-2">
<Label htmlFor="currency">Moneda</Label>
<Select value={formData.currency} onValueChange={(v) => updateFormData('currency', v)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="USD">USD - Dólar Estadounidense</SelectItem>
<SelectItem value="MXN">MXN - Peso Mexicano</SelectItem>
<SelectItem value="COP">COP - Peso Colombiano</SelectItem>
<SelectItem value="PEN">PEN - Sol Peruano</SelectItem>
<SelectItem value="CLP">CLP - Peso Chileno</SelectItem>
<SelectItem value="ARS">ARS - Peso Argentino</SelectItem>
<SelectItem value="DOP">DOP - Peso Dominicano</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="rounded-lg bg-muted/50 p-4 text-sm text-muted-foreground">
<p className="font-medium text-foreground mb-1">Nota de privacidad</p>
<p>
La información de compensación solo será visible para roles autorizados (RR.HH. y Administración).
</p>
</div>
</div>
)}
{/* Step 5: Revisión */}
{currentStep === 'revision' && (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold mb-1">Revisión Final</h2>
<p className="text-sm text-muted-foreground">
Verifica que toda la información sea correcta antes de enviar
</p>
</div>
<div className="space-y-4">
{/* Location Summary */}
<div className="rounded-lg border p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="font-medium flex items-center gap-2">
<MapPin className="h-4 w-4 text-primary" />
Ubicación
</h3>
<Button variant="ghost" size="sm" onClick={() => setCurrentStep('ubicacion')}>
Editar
</Button>
</div>
<div className="grid gap-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">País:</span>
<span>{selectedCountry?.name || '-'}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Ciudad:</span>
<span>{selectedCity?.name || '-'}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Departamento:</span>
<span>{selectedDepartment?.name || '-'}</span>
</div>
</div>
</div>
{/* Vacancy Summary */}
<div className="rounded-lg border p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="font-medium flex items-center gap-2">
<Briefcase className="h-4 w-4 text-primary" />
Vacante
</h3>
<Button variant="ghost" size="sm" onClick={() => setCurrentStep('vacante')}>
Editar
</Button>
</div>
<div className="grid gap-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Posición:</span>
<span>{selectedPosition?.name || '-'}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Cantidad:</span>
<span>{formData.quantity}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Modalidad:</span>
<span className="capitalize">{formData.modalidad}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Tipo:</span>
<span className="capitalize">{formData.tipoContratacion}</span>
</div>
</div>
</div>
{/* Justification Summary */}
<div className="rounded-lg border p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="font-medium flex items-center gap-2">
<FileText className="h-4 w-4 text-primary" />
Justificación
</h3>
<Button variant="ghost" size="sm" onClick={() => setCurrentStep('justificacion')}>
Editar
</Button>
</div>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Urgencia:</span>
<span className="capitalize">{formData.urgency}</span>
</div>
<p className="text-muted-foreground mt-2">{formData.justification || '-'}</p>
</div>
</div>
{/* Compensation Summary */}
{(formData.salaryPackage || formData.budget) && (
<div className="rounded-lg border p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="font-medium flex items-center gap-2">
<DollarSign className="h-4 w-4 text-primary" />
Compensación
</h3>
<Button variant="ghost" size="sm" onClick={() => setCurrentStep('compensacion')}>
Editar
</Button>
</div>
<div className="grid gap-2 text-sm">
{formData.salaryPackage && (
<div className="flex justify-between">
<span className="text-muted-foreground">Paquete:</span>
<span>{formData.salaryPackage}</span>
</div>
)}
{formData.budget && (
<div className="flex justify-between">
<span className="text-muted-foreground">Presupuesto:</span>
<span>{formData.currency} {parseInt(formData.budget).toLocaleString()}</span>
</div>
)}
</div>
</div>
)}
</div>
</div>
)}
{/* Navigation Buttons */}
<Separator className="my-6" />
<div className="flex items-center justify-between">
<div>
{currentStep !== 'ubicacion' && (
<Button variant="outline" onClick={goToPreviousStep}>
<ArrowLeft className="mr-2 h-4 w-4" />
Anterior
</Button>
)}
</div>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={handleSaveDraft}>
<Save className="mr-2 h-4 w-4" />
Guardar Borrador
</Button>
{currentStep !== 'revision' ? (
<Button onClick={goToNextStep}>
Siguiente
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
) : (
<Button onClick={handleSubmit}>
<Send className="mr-2 h-4 w-4" />
Enviar a Aprobación
</Button>
)}
</div>
</div>
</CardContent>
</Card>
</div>
)
}