From 364826c5c862af39a890e0f96e219e5f93fdfd80 Mon Sep 17 00:00:00 2001 From: Tiago Yamamoto Date: Sun, 22 Feb 2026 18:27:30 -0600 Subject: [PATCH] fix(dashboard): align CRUD pages with backend fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../src/app/dashboard/applications/page.tsx | 32 +- frontend/src/app/dashboard/companies/page.tsx | 48 +- frontend/src/app/dashboard/jobs/page.tsx | 563 ++++++++++++------ frontend/src/lib/api.ts | 16 + 4 files changed, 462 insertions(+), 197 deletions(-) diff --git a/frontend/src/app/dashboard/applications/page.tsx b/frontend/src/app/dashboard/applications/page.tsx index 7d5f228..c6e4059 100644 --- a/frontend/src/app/dashboard/applications/page.tsx +++ b/frontend/src/app/dashboard/applications/page.tsx @@ -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() { -
{stats.interview}
-

Interview

+
{stats.shortlisted}
+

Selecionados

-
{stats.accepted}
-

Hired

+
{stats.hired}
+

Contratados

@@ -170,11 +170,11 @@ export default function ApplicationsPage() { All Status - Pending - Reviewing - Interview - Accepted - Rejected + Pendente + Em análise + Selecionado + Contratado + Rejeitado diff --git a/frontend/src/app/dashboard/companies/page.tsx b/frontend/src/app/dashboard/companies/page.tsx index 2bc06af..e7f936a 100644 --- a/frontend/src/app/dashboard/companies/page.tsx +++ b/frontend/src/app/dashboard/companies/page.tsx @@ -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..." /> +
+ + setFormData({ ...formData, logoUrl: e.target.value })} + placeholder="https://example.com/logo.png" + /> +
+
+ + setFormData({ ...formData, yearsInMarket: e.target.value })} + placeholder="Ex: 10" + /> +
@@ -832,6 +860,24 @@ export default function AdminCompaniesPage() { onChange={(e) => setEditFormData({ ...editFormData, description: e.target.value })} /> +
+ + setEditFormData({ ...editFormData, logoUrl: e.target.value })} + placeholder="https://example.com/logo.png" + /> +
+
+ + setEditFormData({ ...editFormData, yearsInMarket: e.target.value })} + placeholder="Ex: 10" + /> +
diff --git a/frontend/src/app/dashboard/jobs/page.tsx b/frontend/src/app/dashboard/jobs/page.tsx index 74fffd1..f63b0ac 100644 --- a/frontend/src/app/dashboard/jobs/page.tsx +++ b/frontend/src/app/dashboard/jobs/page.tsx @@ -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,146 +65,131 @@ export default function AdminJobsPage() { const [jobs, setJobs] = useState([]) const [isViewDialogOpen, setIsViewDialogOpen] = useState(false) const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) - const [selectedJob, setSelectedJob] = useState(null) - const [editForm, setEditForm] = useState<{ title?: string }>({}) + const [selectedJob, setSelectedJob] = useState(null) + const [editForm, setEditForm] = useState(defaultEditForm) const [isLoading, setIsLoading] = useState(true) + const [isSaving, setIsSaving] = useState(false) const [errorMessage, setErrorMessage] = useState(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 () => { - try { - setIsLoading(true) - setErrorMessage(null) - // Fetch with pagination - const jobsData = await adminJobsApi.list({ limit, page }) - 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')) - setJobs([]) - } finally { - setIsLoading(false) - } - } - - loadJobs() - }, [page, limit, t]) - - const jobRows = useMemo( - () => - 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], - ) - - - const filteredJobs = useMemo( - () => - jobRows.filter( - (job) => - job.title.toLowerCase().includes(searchTerm.toLowerCase()) || - job.company.toLowerCase().includes(searchTerm.toLowerCase()), - ), - [jobRows, searchTerm], - ) - - const activeJobs = useMemo( - () => - 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], - ) - - const handleViewJob = (job: AdminJobRow) => { - setSelectedJob(job) - setIsViewDialogOpen(true) - } - - const handleEditJob = (job: AdminJobRow) => { - setSelectedJob(job) - setEditForm({ - title: job.title, - }) - 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) - alert(t('admin.jobs.deleteError')) - } - } - - - const handleSaveEdit = async () => { - if (!selectedJob) return - + const loadJobs = async (targetPage = page) => { try { setIsLoading(true) - await jobsApi.update(selectedJob.id, editForm) - // Reload jobs to get fresh data - const jobsData = await adminJobsApi.list({ limit: 10, page: 1 }) + setErrorMessage(null) + const jobsData = await adminJobsApi.list({ limit, page: targetPage }) setJobs(jobsData.data ?? []) - setIsEditDialogOpen(false) + if (jobsData.pagination) { + setTotalPages(Math.ceil((jobsData.pagination.total || 0) / limit)) + setTotalJobs(jobsData.pagination.total || 0) + } } catch (error) { - console.error("[JOBS_PAGE] Failed to update job:", error) - alert(t('admin.jobs.updateError')) + console.error("Failed to load jobs:", error) + setErrorMessage(t('admin.jobs.table.error')) + setJobs([]) } finally { setIsLoading(false) } } + useEffect(() => { loadJobs() }, [page, limit]) - const handlePreviousPage = () => { - if (page > 1) setPage(page - 1) + 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 handleNextPage = () => { - if (page < totalPages) setPage(page + 1) + 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) => { + if (!confirm(t('admin.jobs.deleteConfirm'))) return + try { + await jobsApi.delete(id) + setJobs((prev) => prev.filter((job) => job.id !== id)) + } catch { + alert(t('admin.jobs.deleteError')) + } + } + + 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 ( @@ -201,7 +210,7 @@ export default function AdminJobsPage() { {/* View Job Dialog */} - + {t('admin.jobs.details.title')} @@ -209,56 +218,262 @@ export default function AdminJobsPage() {
- +

{selectedJob.title}

- -

{selectedJob.company}

+ +

{selectedJob.companyName}

- -

{selectedJob.status}

+ + {selectedJob.status}
- -

{selectedJob.applicationsCount}

+ +

{selectedJob.applicationsCount ?? 0}

+
+
+ +

{selectedJob.employmentType ?? "-"}

+
+
+ +

{selectedJob.workMode ?? "-"}

+
+
+ +

{selectedJob.location ?? "-"}

+
+
+ +

+ {selectedJob.salaryNegotiable + ? "Negociável" + : selectedJob.salaryMin + ? `${selectedJob.currency ?? ""} ${selectedJob.salaryMin}${selectedJob.salaryMax ? ` – ${selectedJob.salaryMax}` : ""}` + : "-"} +

+
+
+ +

{selectedJob.languageLevel || "-"}

+
+
+ +

{selectedJob.visaSupport ? "Sim" : "Não"}

- -
- {selectedJob.description} + +
+ {selectedJob.description || "-"}
)} - + + {selectedJob && ( + + )}
{/* Edit Job Dialog */} - + {t('admin.jobs.edit.title')} {t('admin.jobs.edit.subtitle')}
+ {/* Title + Status */} +
+
+ + setEditForm({ ...editForm, title: e.target.value })} + /> +
+
+ + +
+
+ + setEditForm({ ...editForm, location: e.target.value })} + placeholder="Ex: Tokyo, Japan" + /> +
+
+ + {/* Employment + WorkMode */} +
+
+ + +
+
+ + +
+
+ + {/* Salary */} +
+
+ + +
+
+ + +
+
+ + setEditForm({ ...editForm, salaryMin: e.target.value })} + placeholder="0" + /> +
+
+ + setEditForm({ ...editForm, salaryMax: e.target.value })} + placeholder="0" + /> +
+
+ + {/* Language + WorkingHours */} +
+
+ + +
+
+ + setEditForm({ ...editForm, workingHours: e.target.value })} + placeholder="Ex: 40h/semana" + /> +
+
+ + {/* Toggles */} +
+
+ setEditForm({ ...editForm, salaryNegotiable: v })} + id="edit-negotiable" + /> + +
+
+ setEditForm({ ...editForm, visaSupport: v })} + id="edit-visa" + /> + +
+
+ setEditForm({ ...editForm, isFeatured: v })} + id="edit-featured" + /> + +
+
+ + {/* Description */}
- - setEditForm({ ...editForm, title: e.target.value })} + +