348 lines
17 KiB
TypeScript
348 lines
17 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select"
|
|
import { Input } from "@/components/ui/input"
|
|
import {
|
|
Search,
|
|
Filter,
|
|
Mail,
|
|
Phone,
|
|
FileText,
|
|
Calendar,
|
|
MessageSquare,
|
|
MoreVertical,
|
|
Eye,
|
|
Check,
|
|
X,
|
|
Clock,
|
|
Star,
|
|
} from "lucide-react"
|
|
|
|
// Mock data - in production this would come from API
|
|
const mockApplications = [
|
|
{
|
|
id: "1",
|
|
candidateName: "Ana Silva",
|
|
email: "ana.silva@email.com",
|
|
phone: "+55 11 99999-1111",
|
|
jobTitle: "Senior Full Stack Developer",
|
|
appliedAt: "2 hours ago",
|
|
status: "pending",
|
|
resumeUrl: "https://cdn.gohorsejobs.com/docs/ana_silva_cv.pdf",
|
|
message: "Tenho 5 anos de experiência como desenvolvedora Full Stack...",
|
|
},
|
|
{
|
|
id: "2",
|
|
candidateName: "Carlos Santos",
|
|
email: "carlos.santos@email.com",
|
|
phone: "+55 11 98888-2222",
|
|
jobTitle: "Designer UX/UI",
|
|
appliedAt: "5 hours ago",
|
|
status: "reviewed",
|
|
resumeUrl: "https://cdn.gohorsejobs.com/docs/carlos_santos_portfolio.pdf",
|
|
message: "Designer UX/UI com 3 anos de experiência...",
|
|
},
|
|
{
|
|
id: "3",
|
|
candidateName: "Maria Oliveira",
|
|
email: "maria.oliveira@email.com",
|
|
phone: "+55 21 97777-3333",
|
|
jobTitle: "Product Manager",
|
|
appliedAt: "1 day ago",
|
|
status: "shortlisted",
|
|
resumeUrl: "https://cdn.gohorsejobs.com/docs/maria_oliveira_resume.pdf",
|
|
message: "Product Manager com 4 anos de experiência...",
|
|
},
|
|
{
|
|
id: "4",
|
|
candidateName: "Pedro Costa",
|
|
email: "pedro.costa@email.com",
|
|
phone: "+55 11 96666-4444",
|
|
jobTitle: "Backend Developer",
|
|
appliedAt: "2 days ago",
|
|
status: "hired",
|
|
resumeUrl: "https://cdn.gohorsejobs.com/docs/pedro_costa_cv.pdf",
|
|
message: "Desenvolvedor Backend com experiência em Go...",
|
|
},
|
|
{
|
|
id: "5",
|
|
candidateName: "Juliana Ferreira",
|
|
email: "juliana.ferreira@email.com",
|
|
phone: "+55 11 95555-5555",
|
|
jobTitle: "Data Scientist",
|
|
appliedAt: "3 days ago",
|
|
status: "rejected",
|
|
resumeUrl: "https://cdn.gohorsejobs.com/docs/juliana_ferreira_cv.pdf",
|
|
message: "Data Scientist com mestrado em Machine Learning...",
|
|
},
|
|
]
|
|
|
|
const statusConfig = {
|
|
pending: { label: "Pending", color: "bg-yellow-100 text-yellow-800 border-yellow-200", icon: Clock },
|
|
reviewed: { label: "Reviewed", color: "bg-blue-100 text-blue-800 border-blue-200", icon: Eye },
|
|
shortlisted: { label: "Shortlisted", color: "bg-purple-100 text-purple-800 border-purple-200", icon: Star },
|
|
hired: { label: "Hired", color: "bg-green-100 text-green-800 border-green-200", icon: Check },
|
|
rejected: { label: "Rejected", color: "bg-red-100 text-red-800 border-red-200", icon: X },
|
|
}
|
|
|
|
export default function ApplicationsPage() {
|
|
const [applications, setApplications] = useState(mockApplications)
|
|
const [statusFilter, setStatusFilter] = useState("all")
|
|
const [searchTerm, setSearchTerm] = useState("")
|
|
const [selectedApp, setSelectedApp] = useState<typeof mockApplications[0] | null>(null)
|
|
|
|
const filteredApplications = applications.filter((app) => {
|
|
const matchesStatus = statusFilter === "all" || app.status === statusFilter
|
|
const matchesSearch =
|
|
app.candidateName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
app.jobTitle.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
app.email.toLowerCase().includes(searchTerm.toLowerCase())
|
|
return matchesStatus && matchesSearch
|
|
})
|
|
|
|
const stats = {
|
|
total: applications.length,
|
|
pending: applications.filter((a) => a.status === "pending").length,
|
|
shortlisted: applications.filter((a) => a.status === "shortlisted").length,
|
|
hired: applications.filter((a) => a.status === "hired").length,
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
<div>
|
|
<h1 className="text-2xl sm:text-3xl font-bold text-foreground">
|
|
Applications
|
|
</h1>
|
|
<p className="text-muted-foreground">
|
|
Manage applications for your job postings
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats */}
|
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="text-2xl font-bold">{stats.total}</div>
|
|
<p className="text-xs text-muted-foreground">Total Applications</p>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="text-2xl font-bold text-yellow-600">{stats.pending}</div>
|
|
<p className="text-xs text-muted-foreground">Pending Review</p>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="text-2xl font-bold text-purple-600">{stats.shortlisted}</div>
|
|
<p className="text-xs text-muted-foreground">Shortlisted</p>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="text-2xl font-bold text-green-600">{stats.hired}</div>
|
|
<p className="text-xs text-muted-foreground">Hired</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<div className="flex flex-col sm:flex-row gap-4">
|
|
<div className="relative flex-1">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder="Search by name, job, or email..."
|
|
className="pl-10"
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
/>
|
|
</div>
|
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
|
<SelectTrigger className="w-full sm:w-[180px]">
|
|
<Filter className="h-4 w-4 mr-2" />
|
|
<SelectValue placeholder="Filter by status" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">All Status</SelectItem>
|
|
<SelectItem value="pending">Pending</SelectItem>
|
|
<SelectItem value="reviewed">Reviewed</SelectItem>
|
|
<SelectItem value="shortlisted">Shortlisted</SelectItem>
|
|
<SelectItem value="hired">Hired</SelectItem>
|
|
<SelectItem value="rejected">Rejected</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Applications List */}
|
|
<div className="grid gap-4">
|
|
{filteredApplications.length === 0 ? (
|
|
<Card>
|
|
<CardContent className="p-8 text-center text-muted-foreground">
|
|
No applications found matching your filters.
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
filteredApplications.map((app) => {
|
|
const StatusIcon = statusConfig[app.status as keyof typeof statusConfig].icon
|
|
return (
|
|
<Card
|
|
key={app.id}
|
|
className="hover:border-primary/50 transition-colors cursor-pointer"
|
|
onClick={() => setSelectedApp(app)}
|
|
>
|
|
<CardContent className="p-4 sm:p-6">
|
|
<div className="flex flex-col sm:flex-row gap-4">
|
|
{/* Avatar and Info */}
|
|
<div className="flex items-start gap-4 flex-1">
|
|
<Avatar className="h-12 w-12">
|
|
<AvatarFallback className="bg-primary/10 text-primary">
|
|
{app.candidateName
|
|
.split(" ")
|
|
.map((n) => n[0])
|
|
.join("")
|
|
.toUpperCase()
|
|
.slice(0, 2)}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<h3 className="font-semibold">{app.candidateName}</h3>
|
|
<Badge
|
|
variant="outline"
|
|
className={statusConfig[app.status as keyof typeof statusConfig].color}
|
|
>
|
|
<StatusIcon className="h-3 w-3 mr-1" />
|
|
{statusConfig[app.status as keyof typeof statusConfig].label}
|
|
</Badge>
|
|
</div>
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
Applied for: <span className="font-medium text-foreground">{app.jobTitle}</span>
|
|
</p>
|
|
<div className="flex flex-wrap gap-4 mt-2 text-sm text-muted-foreground">
|
|
<span className="flex items-center gap-1">
|
|
<Mail className="h-4 w-4" />
|
|
{app.email}
|
|
</span>
|
|
<span className="flex items-center gap-1">
|
|
<Phone className="h-4 w-4" />
|
|
{app.phone}
|
|
</span>
|
|
<span className="flex items-center gap-1">
|
|
<Calendar className="h-4 w-4" />
|
|
{app.appliedAt}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="flex items-center gap-2 sm:flex-col">
|
|
<Button variant="outline" size="sm" className="flex-1 sm:flex-none">
|
|
<FileText className="h-4 w-4 mr-2" />
|
|
Resume
|
|
</Button>
|
|
<Button variant="outline" size="sm" className="flex-1 sm:flex-none">
|
|
<MessageSquare className="h-4 w-4 mr-2" />
|
|
Message
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
})
|
|
)}
|
|
</div>
|
|
|
|
{/* Selected Application Modal/Drawer would go here */}
|
|
{selectedApp && (
|
|
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4" onClick={() => setSelectedApp(null)}>
|
|
<Card className="w-full max-w-lg" onClick={(e) => e.stopPropagation()}>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle>Application Details</CardTitle>
|
|
<Button variant="ghost" size="icon" onClick={() => setSelectedApp(null)}>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="flex items-center gap-4">
|
|
<Avatar className="h-16 w-16">
|
|
<AvatarFallback className="bg-primary/10 text-primary text-lg">
|
|
{selectedApp.candidateName
|
|
.split(" ")
|
|
.map((n) => n[0])
|
|
.join("")
|
|
.toUpperCase()
|
|
.slice(0, 2)}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div>
|
|
<h3 className="font-semibold text-lg">{selectedApp.candidateName}</h3>
|
|
<p className="text-sm text-muted-foreground">{selectedApp.jobTitle}</p>
|
|
<Badge
|
|
variant="outline"
|
|
className={statusConfig[selectedApp.status as keyof typeof statusConfig].color + " mt-2"}
|
|
>
|
|
{statusConfig[selectedApp.status as keyof typeof statusConfig].label}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Mail className="h-4 w-4 text-muted-foreground" />
|
|
<a href={`mailto:${selectedApp.email}`} className="text-primary hover:underline">
|
|
{selectedApp.email}
|
|
</a>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Phone className="h-4 w-4 text-muted-foreground" />
|
|
<a href={`tel:${selectedApp.phone}`} className="text-primary hover:underline">
|
|
{selectedApp.phone}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h4 className="font-medium mb-2">Cover Message</h4>
|
|
<p className="text-sm text-muted-foreground bg-muted p-3 rounded-lg">
|
|
{selectedApp.message}
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button className="flex-1" variant="outline">
|
|
<FileText className="h-4 w-4 mr-2" />
|
|
View Resume
|
|
</Button>
|
|
<Button className="flex-1">
|
|
<MessageSquare className="h-4 w-4 mr-2" />
|
|
Contact
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|