fix(dashboard): align CRUD pages with backend fields

jobs/page.tsx:
- Edit dialog now exposes all UpdateJobRequest fields: employmentType,
  workMode, salaryMin/max/type/currency, salaryNegotiable, languageLevel,
  visaSupport, location, status, isFeatured, description
- Fix AdminJob type to include all JobWithCompany fields returned by API
- Fix jobRows mapping that was hardcoding location/type/workMode/isFeatured
- Add isFeatured to CreateJobPayload type

applications/page.tsx:
- Fix status mismatch: reviewing→reviewed, interview→shortlisted, accepted→hired
- Align statusConfig labels/keys with backend constraint (pending/reviewed/
  shortlisted/rejected/hired)
- Update stats counters to use corrected status keys

companies/page.tsx:
- Add logoUrl and yearsInMarket to create and edit forms
- Populate editFormData from company object on edit open
- Send logoUrl/yearsInMarket in update payload

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tiago Yamamoto 2026-02-22 18:27:30 -06:00
parent 0876584499
commit 364826c5c8
4 changed files with 462 additions and 197 deletions

View file

@ -39,11 +39,11 @@ import { applicationsApi, notificationsApi } from "@/lib/api"
import { toast } from "sonner"
const statusConfig = {
pending: { label: "Pending", color: "bg-yellow-100 text-yellow-800 border-yellow-200", icon: Clock },
reviewing: { label: "Reviewing", color: "bg-blue-100 text-blue-800 border-blue-200", icon: Eye },
interview: { label: "Interview", color: "bg-purple-100 text-purple-800 border-purple-200", icon: Star },
accepted: { 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 },
pending: { label: "Pendente", color: "bg-yellow-100 text-yellow-800 border-yellow-200", icon: Clock },
reviewed: { label: "Em análise", color: "bg-blue-100 text-blue-800 border-blue-200", icon: Eye },
shortlisted: { label: "Selecionado", color: "bg-purple-100 text-purple-800 border-purple-200", icon: Star },
hired: { label: "Contratado", color: "bg-green-100 text-green-800 border-green-200", icon: Check },
rejected: { label: "Rejeitado", color: "bg-red-100 text-red-800 border-red-200", icon: X },
}
interface Application {
@ -106,8 +106,8 @@ export default function ApplicationsPage() {
const stats = {
total: applications.length,
pending: applications.filter((a) => a.status === "pending").length,
interview: applications.filter((a) => a.status === "interview").length,
accepted: applications.filter((a) => a.status === "accepted").length,
shortlisted: applications.filter((a) => a.status === "shortlisted").length,
hired: applications.filter((a) => a.status === "hired").length,
}
return (
@ -140,14 +140,14 @@ export default function ApplicationsPage() {
</Card>
<Card>
<CardContent className="p-4">
<div className="text-2xl font-bold text-purple-600">{stats.interview}</div>
<p className="text-xs text-muted-foreground">Interview</p>
<div className="text-2xl font-bold text-purple-600">{stats.shortlisted}</div>
<p className="text-xs text-muted-foreground">Selecionados</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-2xl font-bold text-green-600">{stats.accepted}</div>
<p className="text-xs text-muted-foreground">Hired</p>
<div className="text-2xl font-bold text-green-600">{stats.hired}</div>
<p className="text-xs text-muted-foreground">Contratados</p>
</CardContent>
</Card>
</div>
@ -170,11 +170,11 @@ export default function ApplicationsPage() {
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="pending">Pending</SelectItem>
<SelectItem value="reviewing">Reviewing</SelectItem>
<SelectItem value="interview">Interview</SelectItem>
<SelectItem value="accepted">Accepted</SelectItem>
<SelectItem value="rejected">Rejected</SelectItem>
<SelectItem value="pending">Pendente</SelectItem>
<SelectItem value="reviewed">Em análise</SelectItem>
<SelectItem value="shortlisted">Selecionado</SelectItem>
<SelectItem value="hired">Contratado</SelectItem>
<SelectItem value="rejected">Rejeitado</SelectItem>
</SelectContent>
</Select>
</div>

View file

@ -105,6 +105,8 @@ export default function AdminCompaniesPage() {
website: "",
address: "",
description: "",
logoUrl: "",
yearsInMarket: "",
})
const [editFormData, setEditFormData] = useState({
name: "",
@ -115,6 +117,8 @@ export default function AdminCompaniesPage() {
document: "",
address: "",
description: "",
logoUrl: "",
yearsInMarket: "",
active: false,
verified: false,
})
@ -173,6 +177,8 @@ export default function AdminCompaniesPage() {
website: "",
address: "",
description: "",
logoUrl: "",
yearsInMarket: "",
})
loadCompanies(1) // Reload first page
} catch (error: any) {
@ -240,6 +246,8 @@ export default function AdminCompaniesPage() {
document: company.document || "",
address: company.address || "",
description: company.description || "",
logoUrl: (company as any).logoUrl || "",
yearsInMarket: (company as any).yearsInMarket || "",
active: company.active || false,
verified: company.verified || false,
})
@ -269,7 +277,9 @@ export default function AdminCompaniesPage() {
document: editFormData.document,
address: editFormData.address,
description: editFormData.description,
})
logoUrl: editFormData.logoUrl || undefined,
yearsInMarket: editFormData.yearsInMarket || undefined,
} as any)
toast.success(t('admin.companies.success.updated'))
setIsEditDialogOpen(false)
@ -448,6 +458,24 @@ export default function AdminCompaniesPage() {
placeholder="Company description..."
/>
</div>
<div className="grid gap-2">
<Label htmlFor="logoUrl">Logo URL</Label>
<Input
id="logoUrl"
value={formData.logoUrl}
onChange={(e) => setFormData({ ...formData, logoUrl: e.target.value })}
placeholder="https://example.com/logo.png"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="yearsInMarket">Anos no mercado</Label>
<Input
id="yearsInMarket"
value={formData.yearsInMarket}
onChange={(e) => setFormData({ ...formData, yearsInMarket: e.target.value })}
placeholder="Ex: 10"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>{t('admin.companies.create.cancel')}</Button>
@ -832,6 +860,24 @@ export default function AdminCompaniesPage() {
onChange={(e) => setEditFormData({ ...editFormData, description: e.target.value })}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="edit-logoUrl">Logo URL</Label>
<Input
id="edit-logoUrl"
value={editFormData.logoUrl}
onChange={(e) => setEditFormData({ ...editFormData, logoUrl: e.target.value })}
placeholder="https://example.com/logo.png"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="edit-yearsInMarket">Anos no mercado</Label>
<Input
id="edit-yearsInMarket"
value={editFormData.yearsInMarket}
onChange={(e) => setEditFormData({ ...editFormData, yearsInMarket: e.target.value })}
placeholder="Ex: 10"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsEditDialogOpen(false)}>{t('admin.companies.create.cancel')}</Button>

View file

@ -4,6 +4,7 @@ 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"
@ -16,23 +17,46 @@ import {
DialogTitle,
} from "@/components/ui/dialog"
import { Label } from "@/components/ui/label"
import { Plus, Search, Edit, Trash2, Eye, ChevronLeft, ChevronRight } from "lucide-react"
import { adminJobsApi, adminCompaniesApi, jobsApi, type AdminJob, type AdminCompany } from "@/lib/api"
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"
type AdminJobRow = {
id: string
type EditForm = {
title: string
company: string
location: string
type: string
workMode: string
description: string
requirements: string[]
postedAt: 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
status?: string
applicationsCount?: number
}
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() {
@ -41,33 +65,27 @@ export default function AdminJobsPage() {
const [jobs, setJobs] = useState<AdminJob[]>([])
const [isViewDialogOpen, setIsViewDialogOpen] = useState(false)
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
const [selectedJob, setSelectedJob] = useState<AdminJobRow | null>(null)
const [editForm, setEditForm] = useState<{ title?: string }>({})
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)
// Pagination State
const [page, setPage] = useState(1)
const [limit] = useState(10)
const [totalPages, setTotalPages] = useState(1)
const [totalJobs, setTotalJobs] = useState(0)
useEffect(() => {
const loadJobs = async () => {
const loadJobs = async (targetPage = page) => {
try {
setIsLoading(true)
setErrorMessage(null)
// Fetch with pagination
const jobsData = await adminJobsApi.list({ limit, page })
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)
} else {
// Fallback: simpler pagination if no meta
setTotalPages(1)
}
} catch (error) {
console.error("Failed to load jobs:", error)
setErrorMessage(t('admin.jobs.table.error'))
@ -77,110 +95,101 @@ export default function AdminJobsPage() {
}
}
loadJobs()
}, [page, limit, t])
const jobRows = useMemo<AdminJobRow[]>(
() =>
jobs.map((job) => {
const applicationsCount =
typeof (job as { applicationsCount?: number }).applicationsCount === "number"
? (job as { applicationsCount?: number }).applicationsCount
: 0
return {
id: String(job.id),
title: job.title,
company: job.companyName,
location: "",
type: "full-time" as const,
workMode: "onsite" as const,
description: "",
requirements: [],
postedAt: job.createdAt,
isFeatured: false,
status: job.status,
applicationsCount,
}
}),
[jobs],
)
useEffect(() => { loadJobs() }, [page, limit])
const filteredJobs = useMemo(
() =>
jobRows.filter(
() => jobs.filter(
(job) =>
job.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
job.company.toLowerCase().includes(searchTerm.toLowerCase()),
job.companyName.toLowerCase().includes(searchTerm.toLowerCase()),
),
[jobRows, searchTerm],
[jobs, searchTerm],
)
const activeJobs = useMemo(
() =>
jobs.filter((job) =>
["published", "open", "active"].includes(job.status?.toLowerCase() ?? ""),
).length,
() => jobs.filter((job) => ["published", "open", "active"].includes(job.status?.toLowerCase() ?? "")).length,
[jobs],
)
const totalApplications = useMemo(
() => jobRows.reduce((sum, job) => sum + (job.applicationsCount ?? 0), 0),
[jobRows],
() => jobs.reduce((sum, job) => sum + (job.applicationsCount ?? 0), 0),
[jobs],
)
const handleViewJob = (job: AdminJobRow) => {
const handleViewJob = (job: AdminJob) => {
setSelectedJob(job)
setIsViewDialogOpen(true)
}
const handleEditJob = (job: AdminJobRow) => {
const handleEditJob = (job: AdminJob) => {
setSelectedJob(job)
setEditForm({
title: job.title,
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) => {
console.log("[JOBS_PAGE] handleDeleteJob called with id:", id)
if (!confirm(t('admin.jobs.deleteConfirm'))) return
try {
await jobsApi.delete(id)
setJobs((prevJobs) => prevJobs.filter((job) => job.id !== id))
} catch (error) {
console.error("[JOBS_PAGE] Failed to delete job:", error)
setJobs((prev) => prev.filter((job) => job.id !== id))
} catch {
alert(t('admin.jobs.deleteError'))
}
}
const handleSaveEdit = async () => {
if (!selectedJob) return
setIsSaving(true)
try {
setIsLoading(true)
await jobsApi.update(selectedJob.id, editForm)
// Reload jobs to get fresh data
const jobsData = await adminJobsApi.list({ limit: 10, page: 1 })
setJobs(jobsData.data ?? [])
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 (error) {
console.error("[JOBS_PAGE] Failed to update job:", error)
} catch {
alert(t('admin.jobs.updateError'))
} finally {
setIsLoading(false)
setIsSaving(false)
}
}
const handlePreviousPage = () => {
if (page > 1) setPage(page - 1)
const statusColor = (status: string) => {
switch (status) {
case "open": return "default"
case "draft": return "secondary"
case "closed": case "paused": return "outline"
default: return "outline"
}
const handleNextPage = () => {
if (page < totalPages) setPage(page + 1)
}
return (
@ -201,7 +210,7 @@ export default function AdminJobsPage() {
{/* View Job Dialog */}
<Dialog open={isViewDialogOpen} onOpenChange={setIsViewDialogOpen}>
<DialogContent className="max-w-2xl">
<DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{t('admin.jobs.details.title')}</DialogTitle>
</DialogHeader>
@ -209,56 +218,262 @@ export default function AdminJobsPage() {
<div className="grid gap-4 py-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-muted-foreground">{t('admin.jobs.edit.jobTitle')}</Label>
<Label className="text-muted-foreground text-xs">Título</Label>
<p className="font-medium">{selectedJob.title}</p>
</div>
<div>
<Label className="text-muted-foreground">{t('admin.jobs.table.company')}</Label>
<p className="font-medium">{selectedJob.company}</p>
<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">{t('admin.jobs.table.status')}</Label>
<p><Badge>{selectedJob.status}</Badge></p>
<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">{t('admin.jobs.table.applications')}</Label>
<p className="font-medium">{selectedJob.applicationsCount}</p>
<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">{t('admin.jobs.details.description')}</Label>
<div className="mt-1 p-3 bg-muted rounded-md text-sm whitespace-pre-wrap max-h-60 overflow-y-auto">
{selectedJob.description}
<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 onClick={() => setIsViewDialogOpen(false)}>{t('admin.jobs.details.close')}</Button>
<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">
<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">
<div className="grid gap-2">
<Label htmlFor="edit-title">{t('admin.jobs.edit.jobTitle')}</Label>
{/* 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 || ""}
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}>{t('admin.jobs.edit.save')}</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>
@ -268,7 +483,7 @@ export default function AdminJobsPage() {
<Card>
<CardHeader className="pb-3">
<CardDescription>{t('admin.jobs.stats.total')}</CardDescription>
<CardTitle className="text-3xl">{jobs.length}</CardTitle>
<CardTitle className="text-3xl">{totalJobs}</CardTitle>
</CardHeader>
</Card>
<Card>
@ -293,7 +508,7 @@ export default function AdminJobsPage() {
</Card>
</div>
{/* Search and Filter */}
{/* Search + Table */}
<Card>
<CardHeader>
<div className="flex items-center gap-4">
@ -314,6 +529,7 @@ export default function AdminJobsPage() {
<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>
@ -322,19 +538,17 @@ export default function AdminJobsPage() {
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={5} className="text-center text-muted-foreground">
<TableCell colSpan={6} className="text-center text-muted-foreground">
{t('admin.jobs.table.loading')}
</TableCell>
</TableRow>
) : errorMessage ? (
<TableRow>
<TableCell colSpan={5} className="text-center text-destructive">
{errorMessage}
</TableCell>
<TableCell colSpan={6} className="text-center text-destructive">{errorMessage}</TableCell>
</TableRow>
) : filteredJobs.length === 0 ? (
<TableRow>
<TableCell colSpan={5} className="text-center text-muted-foreground">
<TableCell colSpan={6} className="text-center text-muted-foreground">
{t('admin.jobs.table.empty')}
</TableCell>
</TableRow>
@ -342,10 +556,14 @@ export default function AdminJobsPage() {
filteredJobs.map((job) => (
<TableRow key={job.id}>
<TableCell className="font-medium">{job.title}</TableCell>
<TableCell>{job.company}</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="default">{job.status ?? "Active"}</Badge>
<Badge variant={statusColor(job.status)}>{job.status}</Badge>
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
@ -366,34 +584,19 @@ export default function AdminJobsPage() {
</TableBody>
</Table>
{/* Pagination Controls */}
<div className="flex items-center justify-between space-x-2 py-4">
<div className="text-sm text-muted-foreground">
{/* Page info (optional) */}
Page {page} of {totalPages}
Página {page} de {totalPages}
</div>
<div className="space-x-2">
<Button
variant="outline"
size="sm"
onClick={handlePreviousPage}
disabled={page <= 1 || isLoading}
>
<ChevronLeft className="h-4 w-4" />
Previous
<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={handleNextPage}
disabled={page >= totalPages || isLoading}
>
Next
<ChevronRight className="h-4 w-4" />
<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>

View file

@ -135,9 +135,24 @@ export interface AdminCompany {
export interface AdminJob {
id: string;
companyId: string;
title: string;
description?: string;
companyName: string;
location?: string;
employmentType?: string;
workMode?: string;
workingHours?: string;
salaryMin?: number;
salaryMax?: number;
salaryType?: string;
currency?: string;
salaryNegotiable?: boolean;
languageLevel?: string;
visaSupport?: boolean;
status: string;
isFeatured?: boolean;
applicationsCount?: number;
createdAt: string;
}
@ -385,6 +400,7 @@ export interface CreateJobPayload {
applicationPhone?: string | null;
};
status: 'draft' | 'review' | 'open' | 'paused' | 'closed' | 'published';
isFeatured?: boolean;
}
export const jobsApi = {