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:
parent
0876584499
commit
364826c5c8
4 changed files with 462 additions and 197 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue