621 lines
26 KiB
TypeScript
621 lines
26 KiB
TypeScript
"use client"
|
||
|
||
import { useEffect, useMemo, useState } from "react"
|
||
import Link from "next/link"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Input } from "@/components/ui/input"
|
||
import { Textarea } from "@/components/ui/textarea"
|
||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||
import { Badge } from "@/components/ui/badge"
|
||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogFooter,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from "@/components/ui/dialog"
|
||
import { Label } from "@/components/ui/label"
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||
import { Switch } from "@/components/ui/switch"
|
||
import { Plus, Search, Edit, Trash2, Eye, ChevronLeft, ChevronRight, Loader2 } from "lucide-react"
|
||
import { adminJobsApi, jobsApi, type AdminJob } from "@/lib/api"
|
||
import { useTranslation } from "@/lib/i18n"
|
||
import { ConfirmModal } from "@/components/confirm-modal"
|
||
|
||
type EditForm = {
|
||
title: string
|
||
description: string
|
||
location: string
|
||
employmentType: string
|
||
workMode: string
|
||
workingHours: string
|
||
salaryMin: string
|
||
salaryMax: string
|
||
salaryType: string
|
||
currency: string
|
||
salaryNegotiable: boolean
|
||
languageLevel: string
|
||
visaSupport: boolean
|
||
status: string
|
||
isFeatured: boolean
|
||
}
|
||
|
||
const defaultEditForm: EditForm = {
|
||
title: "",
|
||
description: "",
|
||
location: "",
|
||
employmentType: "",
|
||
workMode: "",
|
||
workingHours: "",
|
||
salaryMin: "",
|
||
salaryMax: "",
|
||
salaryType: "monthly",
|
||
currency: "BRL",
|
||
salaryNegotiable: false,
|
||
languageLevel: "",
|
||
visaSupport: false,
|
||
status: "open",
|
||
isFeatured: false,
|
||
}
|
||
|
||
export default function AdminJobsPage() {
|
||
const { t } = useTranslation()
|
||
const [searchTerm, setSearchTerm] = useState("")
|
||
const [jobs, setJobs] = useState<AdminJob[]>([])
|
||
const [isViewDialogOpen, setIsViewDialogOpen] = useState(false)
|
||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
|
||
const [selectedJob, setSelectedJob] = useState<AdminJob | null>(null)
|
||
const [editForm, setEditForm] = useState<EditForm>(defaultEditForm)
|
||
const [isLoading, setIsLoading] = useState(true)
|
||
const [isSaving, setIsSaving] = useState(false)
|
||
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
||
const [deleteConfirmDialog, setDeleteConfirmDialog] = useState<{ isOpen: boolean, jobId: string | null }>({ isOpen: false, jobId: null })
|
||
|
||
const [page, setPage] = useState(1)
|
||
const [limit] = useState(10)
|
||
const [totalPages, setTotalPages] = useState(1)
|
||
const [totalJobs, setTotalJobs] = useState(0)
|
||
|
||
const loadJobs = async (targetPage = page) => {
|
||
try {
|
||
setIsLoading(true)
|
||
setErrorMessage(null)
|
||
const jobsData = await adminJobsApi.list({ limit, page: targetPage })
|
||
setJobs(jobsData.data ?? [])
|
||
if (jobsData.pagination) {
|
||
setTotalPages(Math.ceil((jobsData.pagination.total || 0) / limit))
|
||
setTotalJobs(jobsData.pagination.total || 0)
|
||
}
|
||
} catch (error) {
|
||
console.error("Failed to load jobs:", error)
|
||
setErrorMessage(t('admin.jobs.table.error'))
|
||
setJobs([])
|
||
} finally {
|
||
setIsLoading(false)
|
||
}
|
||
}
|
||
|
||
useEffect(() => { loadJobs() }, [page, limit])
|
||
|
||
const filteredJobs = useMemo(
|
||
() => jobs.filter(
|
||
(job) =>
|
||
job.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||
job.companyName.toLowerCase().includes(searchTerm.toLowerCase()),
|
||
),
|
||
[jobs, searchTerm],
|
||
)
|
||
|
||
const activeJobs = useMemo(
|
||
() => jobs.filter((job) => ["published", "open", "active"].includes(job.status?.toLowerCase() ?? "")).length,
|
||
[jobs],
|
||
)
|
||
|
||
const totalApplications = useMemo(
|
||
() => jobs.reduce((sum, job) => sum + (job.applicationsCount ?? 0), 0),
|
||
[jobs],
|
||
)
|
||
|
||
const handleViewJob = (job: AdminJob) => {
|
||
setSelectedJob(job)
|
||
setIsViewDialogOpen(true)
|
||
}
|
||
|
||
const handleEditJob = (job: AdminJob) => {
|
||
setSelectedJob(job)
|
||
setEditForm({
|
||
title: job.title ?? "",
|
||
description: job.description ?? "",
|
||
location: job.location ?? "",
|
||
employmentType: job.employmentType ?? "",
|
||
workMode: job.workMode ?? "",
|
||
workingHours: job.workingHours ?? "",
|
||
salaryMin: job.salaryMin != null ? String(job.salaryMin) : "",
|
||
salaryMax: job.salaryMax != null ? String(job.salaryMax) : "",
|
||
salaryType: job.salaryType ?? "monthly",
|
||
currency: job.currency ?? "BRL",
|
||
salaryNegotiable: job.salaryNegotiable ?? false,
|
||
languageLevel: job.languageLevel ?? "",
|
||
visaSupport: job.visaSupport ?? false,
|
||
status: job.status ?? "open",
|
||
isFeatured: job.isFeatured ?? false,
|
||
})
|
||
setIsEditDialogOpen(true)
|
||
}
|
||
|
||
const handleDeleteJob = async (id: string) => {
|
||
setDeleteConfirmDialog({ isOpen: true, jobId: id })
|
||
}
|
||
|
||
const confirmDeleteJob = async () => {
|
||
if (!deleteConfirmDialog.jobId) return
|
||
try {
|
||
await jobsApi.delete(deleteConfirmDialog.jobId)
|
||
setJobs((prev) => prev.filter((job) => job.id !== deleteConfirmDialog.jobId))
|
||
} catch {
|
||
alert(t('admin.jobs.deleteError'))
|
||
} finally {
|
||
setDeleteConfirmDialog({ isOpen: false, jobId: null })
|
||
}
|
||
}
|
||
|
||
const handleSaveEdit = async () => {
|
||
if (!selectedJob) return
|
||
setIsSaving(true)
|
||
try {
|
||
await jobsApi.update(selectedJob.id, {
|
||
title: editForm.title || undefined,
|
||
description: editForm.description || undefined,
|
||
location: editForm.location || undefined,
|
||
employmentType: (editForm.employmentType as any) || undefined,
|
||
workMode: (editForm.workMode as any) || undefined,
|
||
workingHours: editForm.workingHours || undefined,
|
||
salaryMin: editForm.salaryMin ? parseFloat(editForm.salaryMin) : undefined,
|
||
salaryMax: editForm.salaryMax ? parseFloat(editForm.salaryMax) : undefined,
|
||
salaryType: (editForm.salaryType as any) || undefined,
|
||
currency: (editForm.currency as any) || undefined,
|
||
salaryNegotiable: editForm.salaryNegotiable,
|
||
languageLevel: editForm.languageLevel || undefined,
|
||
visaSupport: editForm.visaSupport,
|
||
status: (editForm.status as any) || undefined,
|
||
isFeatured: editForm.isFeatured,
|
||
})
|
||
await loadJobs(page)
|
||
setIsEditDialogOpen(false)
|
||
} catch {
|
||
alert(t('admin.jobs.updateError'))
|
||
} finally {
|
||
setIsSaving(false)
|
||
}
|
||
}
|
||
|
||
const statusColor = (status: string) => {
|
||
switch (status) {
|
||
case "open": return "default"
|
||
case "draft": return "secondary"
|
||
case "closed": case "paused": return "outline"
|
||
default: return "outline"
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-8">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-3xl font-bold text-foreground">{t('admin.jobs.title')}</h1>
|
||
<p className="text-muted-foreground mt-1">{t('admin.jobs.subtitle')}</p>
|
||
</div>
|
||
<Link href="/dashboard/jobs/new">
|
||
<Button className="gap-2">
|
||
<Plus className="h-4 w-4" />
|
||
{t('admin.jobs.newJob')}
|
||
</Button>
|
||
</Link>
|
||
</div>
|
||
|
||
<ConfirmModal
|
||
isOpen={deleteConfirmDialog.isOpen}
|
||
onClose={() => setDeleteConfirmDialog({ isOpen: false, jobId: null })}
|
||
onConfirm={confirmDeleteJob}
|
||
title={t('admin.jobs.deleteConfirm')}
|
||
description="Esta ação não pode ser desfeita e irá excluir a vaga permanentemente."
|
||
/>
|
||
|
||
{/* View Job Dialog */}
|
||
<Dialog open={isViewDialogOpen} onOpenChange={setIsViewDialogOpen}>
|
||
<DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto">
|
||
<DialogHeader>
|
||
<DialogTitle>{t('admin.jobs.details.title')}</DialogTitle>
|
||
<DialogDescription>{t('admin.jobs.details.description')}</DialogDescription>
|
||
</DialogHeader>
|
||
{selectedJob && (
|
||
<div className="grid gap-4 py-4">
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<Label className="text-muted-foreground text-xs">Título</Label>
|
||
<p className="font-medium">{selectedJob.title}</p>
|
||
</div>
|
||
<div>
|
||
<Label className="text-muted-foreground text-xs">{t('admin.jobs.table.company')}</Label>
|
||
<p className="font-medium">{selectedJob.companyName}</p>
|
||
</div>
|
||
<div>
|
||
<Label className="text-muted-foreground text-xs">{t('admin.jobs.table.status')}</Label>
|
||
<Badge variant={statusColor(selectedJob.status)}>{selectedJob.status}</Badge>
|
||
</div>
|
||
<div>
|
||
<Label className="text-muted-foreground text-xs">{t('admin.jobs.table.applications')}</Label>
|
||
<p className="font-medium">{selectedJob.applicationsCount ?? 0}</p>
|
||
</div>
|
||
<div>
|
||
<Label className="text-muted-foreground text-xs">Tipo</Label>
|
||
<p className="text-sm">{selectedJob.employmentType ?? "-"}</p>
|
||
</div>
|
||
<div>
|
||
<Label className="text-muted-foreground text-xs">Modalidade</Label>
|
||
<p className="text-sm">{selectedJob.workMode ?? "-"}</p>
|
||
</div>
|
||
<div>
|
||
<Label className="text-muted-foreground text-xs">Localização</Label>
|
||
<p className="text-sm">{selectedJob.location ?? "-"}</p>
|
||
</div>
|
||
<div>
|
||
<Label className="text-muted-foreground text-xs">Salário</Label>
|
||
<p className="text-sm">
|
||
{selectedJob.salaryNegotiable
|
||
? "Negociável"
|
||
: selectedJob.salaryMin
|
||
? `${selectedJob.currency ?? ""} ${selectedJob.salaryMin}${selectedJob.salaryMax ? ` – ${selectedJob.salaryMax}` : ""}`
|
||
: "-"}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<Label className="text-muted-foreground text-xs">Idioma</Label>
|
||
<p className="text-sm">{selectedJob.languageLevel || "-"}</p>
|
||
</div>
|
||
<div>
|
||
<Label className="text-muted-foreground text-xs">Visto</Label>
|
||
<p className="text-sm">{selectedJob.visaSupport ? "Sim" : "Não"}</p>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<Label className="text-muted-foreground text-xs">{t('admin.jobs.details.description')}</Label>
|
||
<div className="mt-1 p-3 bg-muted rounded-md text-sm whitespace-pre-wrap max-h-48 overflow-y-auto">
|
||
{selectedJob.description || "-"}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
<DialogFooter>
|
||
<Button variant="outline" onClick={() => setIsViewDialogOpen(false)}>{t('admin.jobs.details.close')}</Button>
|
||
{selectedJob && (
|
||
<Button onClick={() => { setIsViewDialogOpen(false); handleEditJob(selectedJob) }}>
|
||
<Edit className="h-4 w-4 mr-2" /> Editar
|
||
</Button>
|
||
)}
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* Edit Job Dialog */}
|
||
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
|
||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||
<DialogHeader>
|
||
<DialogTitle>{t('admin.jobs.edit.title')}</DialogTitle>
|
||
<DialogDescription>{t('admin.jobs.edit.subtitle')}</DialogDescription>
|
||
</DialogHeader>
|
||
<div className="grid gap-4 py-4">
|
||
{/* Title + Status */}
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="col-span-2 grid gap-2">
|
||
<Label htmlFor="edit-title">Título *</Label>
|
||
<Input
|
||
id="edit-title"
|
||
value={editForm.title}
|
||
onChange={(e) => setEditForm({ ...editForm, title: e.target.value })}
|
||
/>
|
||
</div>
|
||
<div className="grid gap-2">
|
||
<Label htmlFor="edit-status">Status</Label>
|
||
<Select value={editForm.status} onValueChange={(v) => setEditForm({ ...editForm, status: v })}>
|
||
<SelectTrigger id="edit-status"><SelectValue /></SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="draft">Rascunho</SelectItem>
|
||
<SelectItem value="review">Em revisão</SelectItem>
|
||
<SelectItem value="open">Aberta</SelectItem>
|
||
<SelectItem value="paused">Pausada</SelectItem>
|
||
<SelectItem value="closed">Fechada</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div className="grid gap-2">
|
||
<Label htmlFor="edit-location">Localização</Label>
|
||
<Input
|
||
id="edit-location"
|
||
value={editForm.location}
|
||
onChange={(e) => setEditForm({ ...editForm, location: e.target.value })}
|
||
placeholder="Ex: Tokyo, Japan"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Employment + WorkMode */}
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="grid gap-2">
|
||
<Label>Tipo de contrato</Label>
|
||
<Select value={editForm.employmentType} onValueChange={(v) => setEditForm({ ...editForm, employmentType: v })}>
|
||
<SelectTrigger><SelectValue placeholder="Selecionar" /></SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="full-time">CLT / Full-time</SelectItem>
|
||
<SelectItem value="part-time">Part-time</SelectItem>
|
||
<SelectItem value="contract">PJ / Contract</SelectItem>
|
||
<SelectItem value="dispatch">Dispatch</SelectItem>
|
||
<SelectItem value="temporary">Temporário</SelectItem>
|
||
<SelectItem value="training">Estágio / Training</SelectItem>
|
||
<SelectItem value="voluntary">Voluntário</SelectItem>
|
||
<SelectItem value="permanent">Efetivo</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div className="grid gap-2">
|
||
<Label>Modalidade</Label>
|
||
<Select value={editForm.workMode} onValueChange={(v) => setEditForm({ ...editForm, workMode: v })}>
|
||
<SelectTrigger><SelectValue placeholder="Selecionar" /></SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="onsite">Presencial</SelectItem>
|
||
<SelectItem value="hybrid">Híbrido</SelectItem>
|
||
<SelectItem value="remote">Remoto</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Salary */}
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="grid gap-2">
|
||
<Label>Moeda</Label>
|
||
<Select value={editForm.currency} onValueChange={(v) => setEditForm({ ...editForm, currency: v })}>
|
||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||
<SelectContent>
|
||
{["BRL", "USD", "EUR", "GBP", "JPY", "CNY", "AED", "CAD", "AUD", "CHF"].map(c => (
|
||
<SelectItem key={c} value={c}>{c}</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div className="grid gap-2">
|
||
<Label>Periodicidade</Label>
|
||
<Select value={editForm.salaryType} onValueChange={(v) => setEditForm({ ...editForm, salaryType: v })}>
|
||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="hourly">Por hora</SelectItem>
|
||
<SelectItem value="daily">Por dia</SelectItem>
|
||
<SelectItem value="weekly">Por semana</SelectItem>
|
||
<SelectItem value="monthly">Mensal</SelectItem>
|
||
<SelectItem value="yearly">Anual</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div className="grid gap-2">
|
||
<Label>Salário mínimo</Label>
|
||
<Input
|
||
type="number"
|
||
value={editForm.salaryMin}
|
||
onChange={(e) => setEditForm({ ...editForm, salaryMin: e.target.value })}
|
||
placeholder="0"
|
||
/>
|
||
</div>
|
||
<div className="grid gap-2">
|
||
<Label>Salário máximo</Label>
|
||
<Input
|
||
type="number"
|
||
value={editForm.salaryMax}
|
||
onChange={(e) => setEditForm({ ...editForm, salaryMax: e.target.value })}
|
||
placeholder="0"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Language + WorkingHours */}
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="grid gap-2">
|
||
<Label>Nível de idioma</Label>
|
||
<Select value={editForm.languageLevel} onValueChange={(v) => setEditForm({ ...editForm, languageLevel: v })}>
|
||
<SelectTrigger><SelectValue placeholder="Selecionar" /></SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="none">Nenhum</SelectItem>
|
||
<SelectItem value="beginner">Iniciante</SelectItem>
|
||
<SelectItem value="N5">JLPT N5</SelectItem>
|
||
<SelectItem value="N4">JLPT N4</SelectItem>
|
||
<SelectItem value="N3">JLPT N3</SelectItem>
|
||
<SelectItem value="N2">JLPT N2</SelectItem>
|
||
<SelectItem value="N1">JLPT N1</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div className="grid gap-2">
|
||
<Label>Carga horária</Label>
|
||
<Input
|
||
value={editForm.workingHours}
|
||
onChange={(e) => setEditForm({ ...editForm, workingHours: e.target.value })}
|
||
placeholder="Ex: 40h/semana"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Toggles */}
|
||
<div className="flex items-center gap-6 border rounded-md p-4">
|
||
<div className="flex items-center gap-2">
|
||
<Switch
|
||
checked={editForm.salaryNegotiable}
|
||
onCheckedChange={(v) => setEditForm({ ...editForm, salaryNegotiable: v })}
|
||
id="edit-negotiable"
|
||
/>
|
||
<Label htmlFor="edit-negotiable">Salário negociável</Label>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Switch
|
||
checked={editForm.visaSupport}
|
||
onCheckedChange={(v) => setEditForm({ ...editForm, visaSupport: v })}
|
||
id="edit-visa"
|
||
/>
|
||
<Label htmlFor="edit-visa">Suporte de visto</Label>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Switch
|
||
checked={editForm.isFeatured}
|
||
onCheckedChange={(v) => setEditForm({ ...editForm, isFeatured: v })}
|
||
id="edit-featured"
|
||
/>
|
||
<Label htmlFor="edit-featured">Destaque</Label>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Description */}
|
||
<div className="grid gap-2">
|
||
<Label htmlFor="edit-desc">Descrição</Label>
|
||
<Textarea
|
||
id="edit-desc"
|
||
rows={5}
|
||
value={editForm.description}
|
||
onChange={(e) => setEditForm({ ...editForm, description: e.target.value })}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<DialogFooter>
|
||
<Button variant="outline" onClick={() => setIsEditDialogOpen(false)}>{t('admin.jobs.edit.cancel')}</Button>
|
||
<Button onClick={handleSaveEdit} disabled={isSaving}>
|
||
{isSaving && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||
{t('admin.jobs.edit.save')}
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* Stats */}
|
||
<div className="grid gap-4 md:grid-cols-4">
|
||
<Card>
|
||
<CardHeader className="pb-3">
|
||
<CardDescription>{t('admin.jobs.stats.total')}</CardDescription>
|
||
<CardTitle className="text-3xl">{totalJobs}</CardTitle>
|
||
</CardHeader>
|
||
</Card>
|
||
<Card>
|
||
<CardHeader className="pb-3">
|
||
<CardDescription>{t('admin.jobs.stats.active')}</CardDescription>
|
||
<CardTitle className="text-3xl">{activeJobs}</CardTitle>
|
||
</CardHeader>
|
||
</Card>
|
||
<Card>
|
||
<CardHeader className="pb-3">
|
||
<CardDescription>{t('admin.jobs.stats.applications')}</CardDescription>
|
||
<CardTitle className="text-3xl">{totalApplications}</CardTitle>
|
||
</CardHeader>
|
||
</Card>
|
||
<Card>
|
||
<CardHeader className="pb-3">
|
||
<CardDescription>{t('admin.jobs.stats.conversion')}</CardDescription>
|
||
<CardTitle className="text-3xl">
|
||
{jobs.length > 0 ? Math.round((activeJobs / jobs.length) * 100) : 0}%
|
||
</CardTitle>
|
||
</CardHeader>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* Search + Table */}
|
||
<Card>
|
||
<CardHeader>
|
||
<div className="flex items-center 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={t('admin.jobs.searchPlaceholder')}
|
||
value={searchTerm}
|
||
onChange={(e) => setSearchTerm(e.target.value)}
|
||
className="pl-10"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead>{t('admin.jobs.table.role')}</TableHead>
|
||
<TableHead>{t('admin.jobs.table.company')}</TableHead>
|
||
<TableHead>Tipo / Modalidade</TableHead>
|
||
<TableHead>{t('admin.jobs.table.applications')}</TableHead>
|
||
<TableHead>{t('admin.jobs.table.status')}</TableHead>
|
||
<TableHead className="text-right">{t('admin.jobs.table.actions')}</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{isLoading ? (
|
||
<TableRow>
|
||
<TableCell colSpan={6} className="text-center text-muted-foreground">
|
||
{t('admin.jobs.table.loading')}
|
||
</TableCell>
|
||
</TableRow>
|
||
) : errorMessage ? (
|
||
<TableRow>
|
||
<TableCell colSpan={6} className="text-center text-destructive">{errorMessage}</TableCell>
|
||
</TableRow>
|
||
) : filteredJobs.length === 0 ? (
|
||
<TableRow>
|
||
<TableCell colSpan={6} className="text-center text-muted-foreground">
|
||
{t('admin.jobs.table.empty')}
|
||
</TableCell>
|
||
</TableRow>
|
||
) : (
|
||
filteredJobs.map((job) => (
|
||
<TableRow key={job.id}>
|
||
<TableCell className="font-medium">{job.title}</TableCell>
|
||
<TableCell>{job.companyName}</TableCell>
|
||
<TableCell className="text-sm text-muted-foreground">
|
||
{job.employmentType && <span>{job.employmentType}</span>}
|
||
{job.workMode && <span className="ml-1">· {job.workMode}</span>}
|
||
</TableCell>
|
||
<TableCell>{job.applicationsCount ?? 0}</TableCell>
|
||
<TableCell>
|
||
<Badge variant={statusColor(job.status)}>{job.status}</Badge>
|
||
</TableCell>
|
||
<TableCell className="text-right">
|
||
<div className="flex items-center justify-end gap-2">
|
||
<Button variant="ghost" size="icon" onClick={() => handleViewJob(job)}>
|
||
<Eye className="h-4 w-4" />
|
||
</Button>
|
||
<Button variant="ghost" size="icon" onClick={() => handleEditJob(job)}>
|
||
<Edit className="h-4 w-4" />
|
||
</Button>
|
||
<Button variant="ghost" size="icon" onClick={() => handleDeleteJob(job.id)}>
|
||
<Trash2 className="h-4 w-4 text-destructive" />
|
||
</Button>
|
||
</div>
|
||
</TableCell>
|
||
</TableRow>
|
||
))
|
||
)}
|
||
</TableBody>
|
||
</Table>
|
||
|
||
<div className="flex items-center justify-between space-x-2 py-4">
|
||
<div className="text-sm text-muted-foreground">
|
||
Página {page} de {totalPages}
|
||
</div>
|
||
<div className="space-x-2">
|
||
<Button variant="outline" size="sm" onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page <= 1 || isLoading}>
|
||
<ChevronLeft className="h-4 w-4" /> Anterior
|
||
</Button>
|
||
<Button variant="outline" size="sm" onClick={() => setPage(p => Math.min(totalPages, p + 1))} disabled={page >= totalPages || isLoading}>
|
||
Próxima <ChevronRight className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
)
|
||
}
|