Initial commit: Proyecto portal-requisicion-vacantes

This commit is contained in:
Isaac_Aracena
2026-06-12 16:08:49 -04:00
commit b5a16d8daf
112 changed files with 18334 additions and 0 deletions
+384
View File
@@ -0,0 +1,384 @@
'use client'
import * as React from 'react'
import Link from 'next/link'
import { useRouter, useSearchParams } 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 { Input } from '@/components/ui/input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import {
PlusCircle,
Search,
Filter,
MoreHorizontal,
Eye,
Edit,
Clock,
Users,
MapPin,
Building2,
AlertTriangle,
CheckCircle2,
XCircle,
ArrowUpDown,
Download,
RefreshCw,
} from 'lucide-react'
import { cn } from '@/lib/utils'
import { requisitions, countries } from '@/lib/mock-data'
import { STATUS_CONFIG, URGENCY_CONFIG, type Requisition, type RequisitionStatus } from '@/lib/types'
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: 'short',
year: 'numeric',
})
}
interface RequisitionRowProps {
requisition: Requisition
}
function RequisitionTableRow({ requisition }: RequisitionRowProps) {
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
return (
<TableRow className="group">
<TableCell className="font-medium">
<Link href={`/requisiciones/${requisition.id}`} className="hover:text-primary hover:underline">
{requisition.id}
</Link>
</TableCell>
<TableCell>
<div className="space-y-1">
<p className="font-medium text-sm">{requisition.positionName}</p>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<MapPin className="h-3 w-3" />
{requisition.cityName}, {requisition.countryName}
</div>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Users className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">{requisition.vacanciesFilled}</span>
<span className="text-muted-foreground">/ {requisition.quantity}</span>
</div>
</TableCell>
<TableCell>
<Badge variant="outline" className={cn('text-xs', status.color, status.bgColor, status.borderColor)}>
{status.label}
</Badge>
</TableCell>
<TableCell>
<Badge variant="outline" className={cn('text-xs', urgency.color, urgency.bgColor)}>
{urgency.label}
</Badge>
</TableCell>
<TableCell>
<div className="flex items-center gap-2 text-sm">
<Building2 className="h-4 w-4 text-muted-foreground" />
{requisition.departmentName}
</div>
</TableCell>
<TableCell>
<div className={cn(
'flex items-center gap-1.5 text-sm',
isOverdue && 'text-red-600 dark:text-red-400',
isAtRisk && !isOverdue && 'text-amber-600 dark:text-amber-400'
)}>
{isOverdue ? (
<AlertTriangle className="h-4 w-4" />
) : isAtRisk ? (
<Clock className="h-4 w-4" />
) : (
<Clock className="h-4 w-4 text-muted-foreground" />
)}
<span>
{isOverdue
? `${Math.abs(daysLeft)}d vencido`
: `${daysLeft}d`
}
</span>
</div>
</TableCell>
<TableCell>
<span className="text-sm text-muted-foreground">{formatDate(requisition.createdAt)}</span>
</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8 opacity-0 group-hover:opacity-100 transition-opacity">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Acciones</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link href={`/requisiciones/${requisition.id}`}>
<Eye className="mr-2 h-4 w-4" />
Ver detalles
</Link>
</DropdownMenuItem>
{requisition.status === 'borrador' && (
<DropdownMenuItem asChild>
<Link href={`/requisiciones/${requisition.id}/editar`}>
<Edit className="mr-2 h-4 w-4" />
Editar
</Link>
</DropdownMenuItem>
)}
{requisition.status === 'correccion_requerida' && (
<DropdownMenuItem asChild>
<Link href={`/requisiciones/${requisition.id}/editar`}>
<RefreshCw className="mr-2 h-4 w-4" />
Corregir y reenviar
</Link>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
)
}
export function RequisitionsListPage() {
const router = useRouter()
const searchParams = useSearchParams()
const [searchTerm, setSearchTerm] = React.useState('')
const [statusFilter, setStatusFilter] = React.useState<string>('all')
const [countryFilter, setCountryFilter] = React.useState<string>('all')
const [activeTab, setActiveTab] = React.useState('todas')
// Check for filter param
React.useEffect(() => {
const filter = searchParams.get('filter')
if (filter === 'urgent') {
setActiveTab('urgentes')
}
}, [searchParams])
const filteredRequisitions = React.useMemo(() => {
let filtered = [...requisitions]
// Search filter
if (searchTerm) {
const term = searchTerm.toLowerCase()
filtered = filtered.filter(r =>
r.id.toLowerCase().includes(term) ||
r.positionName.toLowerCase().includes(term) ||
r.cityName.toLowerCase().includes(term) ||
r.requesterName.toLowerCase().includes(term)
)
}
// Status filter
if (statusFilter !== 'all') {
filtered = filtered.filter(r => r.status === statusFilter)
}
// Country filter
if (countryFilter !== 'all') {
filtered = filtered.filter(r => r.countryId === countryFilter)
}
// Tab filter
if (activeTab === 'abiertas') {
filtered = filtered.filter(r => !['cerrado', 'cancelado'].includes(r.status))
} else if (activeTab === 'urgentes') {
filtered = filtered.filter(r => {
const daysLeft = getDaysUntilDeadline(r.hardDeadline)
return !['cerrado', 'cancelado'].includes(r.status) && daysLeft <= 7
})
} else if (activeTab === 'cerradas') {
filtered = filtered.filter(r => ['cerrado', 'cancelado'].includes(r.status))
}
// Sort by date (newest first)
filtered.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
return filtered
}, [searchTerm, statusFilter, countryFilter, activeTab])
const stats = React.useMemo(() => {
const open = requisitions.filter(r => !['cerrado', 'cancelado'].includes(r.status))
const urgent = open.filter(r => getDaysUntilDeadline(r.hardDeadline) <= 7)
const closed = requisitions.filter(r => ['cerrado', 'cancelado'].includes(r.status))
return {
all: requisitions.length,
open: open.length,
urgent: urgent.length,
closed: closed.length,
}
}, [])
return (
<div className="p-6 space-y-6">
{/* Header */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-2xl font-bold tracking-tight">Requisiciones</h1>
<p className="text-muted-foreground">
Gestiona y da seguimiento a todas las requisiciones de vacantes
</p>
</div>
<Button asChild>
<Link href="/requisiciones/nueva">
<PlusCircle className="mr-2 h-4 w-4" />
Nueva Requisición
</Link>
</Button>
</div>
{/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-4 lg:w-[400px]">
<TabsTrigger value="todas" className="text-xs sm:text-sm">
Todas ({stats.all})
</TabsTrigger>
<TabsTrigger value="abiertas" className="text-xs sm:text-sm">
Abiertas ({stats.open})
</TabsTrigger>
<TabsTrigger value="urgentes" className="text-xs sm:text-sm">
<AlertTriangle className="mr-1 h-3 w-3" />
Urgentes ({stats.urgent})
</TabsTrigger>
<TabsTrigger value="cerradas" className="text-xs sm:text-sm">
Cerradas ({stats.closed})
</TabsTrigger>
</TabsList>
<Card className="mt-4">
<CardHeader className="pb-4">
{/* Filters */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar por ID, posición, ciudad..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-9"
/>
</div>
<div className="flex items-center gap-2">
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[180px] sm:w-[220px]">
<Filter className="mr-2 h-4 w-4" />
<SelectValue placeholder="Estado" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos los estados</SelectItem>
{Object.entries(STATUS_CONFIG).map(([key, config]) => (
<SelectItem key={key} value={key}>
{config.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={countryFilter} onValueChange={setCountryFilter}>
<SelectTrigger className="w-[160px] sm:w-[200px]">
<SelectValue placeholder="País" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos los países</SelectItem>
{countries.map((country) => (
<SelectItem key={country.id} value={country.id}>
{country.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Button variant="outline" size="icon">
<Download className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<TabsContent value={activeTab} className="mt-0">
{filteredRequisitions.length > 0 ? (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[120px]">ID</TableHead>
<TableHead>Posición</TableHead>
<TableHead className="w-[100px]">Vacantes</TableHead>
<TableHead className="w-[160px]">Estado</TableHead>
<TableHead className="w-[100px]">Urgencia</TableHead>
<TableHead>Departamento</TableHead>
<TableHead className="w-[110px]">
<div className="flex items-center gap-1">
SLA
<ArrowUpDown className="h-3 w-3" />
</div>
</TableHead>
<TableHead className="w-[110px]">Creada</TableHead>
<TableHead className="w-[50px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredRequisitions.map((requisition) => (
<RequisitionTableRow key={requisition.id} requisition={requisition} />
))}
</TableBody>
</Table>
</div>
) : (
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="rounded-full bg-muted p-3 mb-4">
<Search className="h-6 w-6 text-muted-foreground" />
</div>
<h3 className="text-lg font-medium">No se encontraron requisiciones</h3>
<p className="text-sm text-muted-foreground mt-1">
Intenta ajustar los filtros de búsqueda
</p>
</div>
)}
</TabsContent>
</CardContent>
</Card>
</Tabs>
</div>
)
}