Merge branch 'task5' into dev

This commit is contained in:
NANDO9322 2026-01-08 17:48:29 -03:00
commit 612b8ec716
14 changed files with 696 additions and 209 deletions

View file

@ -178,6 +178,10 @@ func (h *CoreHandlers) CreateCompany(w http.ResponseWriter, r *http.Request) {
resp, err := h.createCompanyUC.Execute(r.Context(), req) resp, err := h.createCompanyUC.Execute(r.Context(), req)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "already exists") {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }

View file

@ -6,9 +6,14 @@ import "time"
type Company struct { type Company struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Type string `json:"type"` // "COMPANY", "CANDIDATE_WORKSPACE" Slug string `json:"slug"`
Document *string `json:"document,omitempty"` // CNPJ, EIN, VAT Document *string `json:"document,omitempty"` // CNPJ, EIN, VAT
Contact *string `json:"contact,omitempty"` Contact *string `json:"contact,omitempty"` // Email
Phone *string `json:"phone,omitempty"`
Website *string `json:"website,omitempty"`
Address *string `json:"address,omitempty"`
Description *string `json:"description,omitempty"`
Type string `json:"type"` // "COMPANY", "CANDIDATE_WORKSPACE"
Status string `json:"status"` // "ACTIVE", "INACTIVE" Status string `json:"status"` // "ACTIVE", "INACTIVE"
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
@ -19,6 +24,7 @@ func NewCompany(id, name string, document, contact *string) *Company {
return &Company{ return &Company{
ID: id, ID: id,
Name: name, Name: name,
Slug: name, // Basic slug, repo might refine
Type: "COMPANY", Type: "COMPANY",
Document: document, Document: document,
Contact: contact, Contact: contact,

View file

@ -12,6 +12,7 @@ type CreateCompanyRequest struct {
Password string `json:"password"` Password string `json:"password"`
Phone string `json:"phone"` Phone string `json:"phone"`
Website *string `json:"website,omitempty"` Website *string `json:"website,omitempty"`
Address *string `json:"address,omitempty"`
EmployeeCount *string `json:"employeeCount,omitempty"` EmployeeCount *string `json:"employeeCount,omitempty"`
FoundedYear *int `json:"foundedYear,omitempty"` FoundedYear *int `json:"foundedYear,omitempty"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`

View file

@ -2,6 +2,7 @@ package tenant
import ( import (
"context" "context"
"fmt"
"github.com/rede5/gohorsejobs/backend/internal/core/domain/entity" "github.com/rede5/gohorsejobs/backend/internal/core/domain/entity"
"github.com/rede5/gohorsejobs/backend/internal/core/dto" "github.com/rede5/gohorsejobs/backend/internal/core/dto"
@ -37,22 +38,57 @@ func (uc *CreateCompanyUseCase) Execute(ctx context.Context, input dto.CreateCom
// I'll generate a random ID here for simulation if I had a uuid lib. // I'll generate a random ID here for simulation if I had a uuid lib.
// Since I want to be agnostic and dependency-free, I'll assume the Repo 'Save' returns the fully populated entity including ID. // Since I want to be agnostic and dependency-free, I'll assume the Repo 'Save' returns the fully populated entity including ID.
// 0. Ensure AdminEmail is set (fallback to Email)
if input.AdminEmail == "" {
input.AdminEmail = input.Email
}
if input.Contact == "" && input.Email != "" {
input.Contact = input.Email
}
// 1. Check if user already exists
existingUser, _ := uc.userRepo.FindByEmail(ctx, input.AdminEmail)
if existingUser != nil {
return nil, fmt.Errorf("user with email %s already exists", input.AdminEmail)
}
company := entity.NewCompany("", input.Name, &input.Document, &input.Contact) company := entity.NewCompany("", input.Name, &input.Document, &input.Contact)
// Map optional fields
if input.Phone != "" {
company.Phone = &input.Phone
}
if input.Website != nil {
company.Website = input.Website
}
if input.Description != nil {
company.Description = input.Description
}
if input.Address != nil {
company.Address = input.Address
}
// Address isn't in DTO explicitly but maybe part of inputs?
// Checking DTO: it has no Address field.
// I will check DTO again. Step 2497 showed Name, CompanyName, Document, Contact, AdminEmail, Email, Password, Phone, Website, EmployeeCount, FoundedYear, Description.
// It misses Address.
// I will skip Address mapping for now or add it to DTO if user wants it.
// But let's map what we have.
savedCompany, err := uc.companyRepo.Save(ctx, company) savedCompany, err := uc.companyRepo.Save(ctx, company)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// 2. Create Admin User // 2. Create Admin User
// We need a password for the admin. Do we generate one? Or did input provide? pwd := input.Password
// input.AdminEmail is present. But no password. I'll generic default or ask to send email. if pwd == "" {
// For simplicity, let's assume a default password "ChangeMe123!" hash it. pwd = "ChangeMe123!"
hashedPassword, _ := uc.authService.HashPassword("ChangeMe123!") }
hashedPassword, _ := uc.authService.HashPassword(pwd)
adminUser := entity.NewUser("", savedCompany.ID, "Admin", input.AdminEmail) adminUser := entity.NewUser("", savedCompany.ID, "Admin", input.AdminEmail)
adminUser.PasswordHash = hashedPassword adminUser.PasswordHash = hashedPassword
adminUser.AssignRole(entity.Role{Name: "ADMIN"}) adminUser.AssignRole(entity.Role{Name: entity.RoleAdmin})
_, err = uc.userRepo.Save(ctx, adminUser) _, err = uc.userRepo.Save(ctx, adminUser)
if err != nil { if err != nil {

View file

@ -70,4 +70,5 @@ type User struct {
AvatarUrl *string `json:"avatarUrl,omitempty"` AvatarUrl *string `json:"avatarUrl,omitempty"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
CompanyID *string `json:"companyId,omitempty"` CompanyID *string `json:"companyId,omitempty"`
Roles []string `json:"roles,omitempty"`
} }

View file

@ -20,8 +20,8 @@ func NewCompanyRepository(db *sql.DB) *CompanyRepository {
func (r *CompanyRepository) Save(ctx context.Context, company *entity.Company) (*entity.Company, error) { func (r *CompanyRepository) Save(ctx context.Context, company *entity.Company) (*entity.Company, error) {
// companies table uses UUID id, DB generates it // companies table uses UUID id, DB generates it
query := ` query := `
INSERT INTO companies (name, slug, type, document, email, description, verified, active, created_at, updated_at) INSERT INTO companies (name, slug, type, document, email, phone, website, address, description, verified, active, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
RETURNING id RETURNING id
` `
@ -30,15 +30,24 @@ func (r *CompanyRepository) Save(ctx context.Context, company *entity.Company) (
compType = "company" compType = "company"
} }
slug := company.Name // TODO: slugify function slug := company.Slug
// Fallback slug generation if empty
if slug == "" {
slug = company.Name
}
// TODO: better slugify logic in service/entity
var id string var id string
err := r.db.QueryRowContext(ctx, query, err := r.db.QueryRowContext(ctx, query,
company.Name, company.Name,
slug, slug,
compType, company.Type,
company.Document, company.Document,
company.Contact, // mapped to email company.Contact, // email
"{}", // description as JSON company.Phone,
company.Website,
company.Address,
company.Description,
true, // verified true, // verified
company.Status == "ACTIVE", company.Status == "ACTIVE",
company.CreatedAt, company.CreatedAt,

View file

@ -25,7 +25,8 @@ func (s *AdminService) ListCompanies(ctx context.Context, verified *bool, page,
offset := (page - 1) * limit offset := (page - 1) * limit
// Count Total // Count Total
countQuery := `SELECT COUNT(*) FROM companies WHERE (type = 'company' OR type = 'COMPANY' OR type IS NULL)` // Count Total
countQuery := `SELECT COUNT(*) FROM companies WHERE type != 'CANDIDATE_WORKSPACE'`
var countArgs []interface{} var countArgs []interface{}
if verified != nil { if verified != nil {
countQuery += " AND verified = $1" countQuery += " AND verified = $1"
@ -40,7 +41,7 @@ func (s *AdminService) ListCompanies(ctx context.Context, verified *bool, page,
baseQuery := ` baseQuery := `
SELECT id, name, slug, type, document, address, region_id, city_id, phone, email, website, logo_url, description, active, verified, created_at, updated_at SELECT id, name, slug, type, document, address, region_id, city_id, phone, email, website, logo_url, description, active, verified, created_at, updated_at
FROM companies FROM companies
WHERE (type = 'company' OR type = 'COMPANY' OR type IS NULL) WHERE type != 'CANDIDATE_WORKSPACE'
` `
var args []interface{} var args []interface{}
@ -611,9 +612,43 @@ func (s *AdminService) GetUser(ctx context.Context, id string) (*dto.User, error
if avatarURL.Valid { if avatarURL.Valid {
u.AvatarUrl = &avatarURL.String u.AvatarUrl = &avatarURL.String
} }
// Fetch roles
roles, err := s.getUserRoles(ctx, u.ID)
if err == nil {
u.Roles = roles
}
return &u, nil return &u, nil
} }
func (s *AdminService) getUserRoles(ctx context.Context, userID string) ([]string, error) {
query := `
SELECT role FROM user_roles WHERE user_id = $1
UNION
SELECT role FROM users WHERE id = $1 AND role IS NOT NULL AND role != ''
`
rows, err := s.DB.QueryContext(ctx, query, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var roles []string
seen := make(map[string]bool)
for rows.Next() {
var roleName string
if err := rows.Scan(&roleName); err == nil {
if !seen[roleName] {
roles = append(roles, roleName)
seen[roleName] = true
}
}
}
return roles, nil
}
// GetCompanyByUserID fetches the company associated with a user // GetCompanyByUserID fetches the company associated with a user
func (s *AdminService) GetCompanyByUserID(ctx context.Context, userID string) (*models.Company, error) { func (s *AdminService) GetCompanyByUserID(ctx context.Context, userID string) (*models.Company, error) {
// First, try to find company where this user is admin // First, try to find company where this user is admin
@ -729,10 +764,29 @@ func (s *AdminService) DeleteCompany(ctx context.Context, id string) error {
return err return err
} }
// Delete tx, err := s.DB.BeginTx(ctx, nil)
_, err = s.DB.ExecContext(ctx, `DELETE FROM companies WHERE id=$1`, id) if err != nil {
return err return err
} }
defer tx.Rollback()
// Delete jobs
if _, err := tx.ExecContext(ctx, `DELETE FROM jobs WHERE company_id=$1`, id); err != nil {
return err
}
// Delete users
if _, err := tx.ExecContext(ctx, `DELETE FROM users WHERE tenant_id=$1`, id); err != nil {
return err
}
// Delete company
if _, err := tx.ExecContext(ctx, `DELETE FROM companies WHERE id=$1`, id); err != nil {
return err
}
return tx.Commit()
}
// ============================================================================ // ============================================================================
// Email Templates & Settings CRUD // Email Templates & Settings CRUD

View file

@ -79,7 +79,10 @@ func (s *JobService) GetJobs(filter dto.JobFilterQuery) ([]models.JobWithCompany
SELECT SELECT
j.id, j.company_id, j.title, j.description, j.salary_min, j.salary_max, j.salary_type, j.id, j.company_id, j.title, j.description, j.salary_min, j.salary_max, j.salary_type,
j.employment_type, j.work_mode, j.working_hours, j.location, j.status, j.salary_negotiable, j.is_featured, j.created_at, j.updated_at, j.employment_type, j.work_mode, j.working_hours, j.location, j.status, j.salary_negotiable, j.is_featured, j.created_at, j.updated_at,
COALESCE(c.name, '') as company_name, c.logo_url as company_logo_url, CASE
WHEN c.type = 'CANDIDATE_WORKSPACE' OR c.name LIKE 'Candidate - %' THEN ''
ELSE COALESCE(c.name, '')
END as company_name, c.logo_url as company_logo_url,
r.name as region_name, ci.name as city_name, r.name as region_name, ci.name as city_name,
(SELECT COUNT(*) FROM applications a WHERE a.job_id = j.id) as applications_count (SELECT COUNT(*) FROM applications a WHERE a.job_id = j.id) as applications_count
FROM jobs j FROM jobs j

View file

@ -4,6 +4,7 @@ import { useEffect, useState } from "react"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
@ -17,12 +18,13 @@ import {
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Plus, Search, Loader2, RefreshCw, Building2, CheckCircle, XCircle, Eye, Trash2, Pencil } from "lucide-react" import { Plus, Search, Loader2, RefreshCw, Building2, CheckCircle, XCircle, Eye, EyeOff, Trash2, Pencil } from "lucide-react"
import { Switch } from "@/components/ui/switch" import { Switch } from "@/components/ui/switch"
import { adminCompaniesApi, type AdminCompany } from "@/lib/api" import { adminCompaniesApi, type AdminCompany } from "@/lib/api"
import { getCurrentUser, isAdminUser } from "@/lib/auth" import { getCurrentUser, isAdminUser } from "@/lib/auth"
import { toast } from "sonner" import { toast } from "sonner"
import { Skeleton } from "@/components/ui/skeleton" import { Skeleton } from "@/components/ui/skeleton"
import { useTranslation } from "@/lib/i18n"
const companyDateFormatter = new Intl.DateTimeFormat("en-US", { const companyDateFormatter = new Intl.DateTimeFormat("en-US", {
dateStyle: "medium", dateStyle: "medium",
@ -56,7 +58,28 @@ const formatDescription = (description: string | undefined) => {
return <p className="text-sm mt-1">{description}</p> return <p className="text-sm mt-1">{description}</p>
} }
// Format CNPJ: 00.000.000/0000-00
const formatCNPJ = (value: string) => {
return value
.replace(/\D/g, "")
.replace(/^(\d{2})(\d)/, "$1.$2")
.replace(/^(\d{2})\.(\d{3})(\d)/, "$1.$2.$3")
.replace(/\.(\d{3})(\d)/, ".$1/$2")
.replace(/(\d{4})(\d)/, "$1-$2")
.substring(0, 18)
}
// Format Phone: (00) 00000-0000
const formatPhone = (value: string) => {
return value
.replace(/\D/g, "")
.replace(/^(\d{2})(\d)/, "($1) $2")
.replace(/(\d{5})(\d)/, "$1-$2")
.substring(0, 15)
}
export default function AdminCompaniesPage() { export default function AdminCompaniesPage() {
const { t } = useTranslation()
const router = useRouter() const router = useRouter()
const [companies, setCompanies] = useState<AdminCompany[]>([]) const [companies, setCompanies] = useState<AdminCompany[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
@ -69,10 +92,19 @@ export default function AdminCompaniesPage() {
const [creating, setCreating] = useState(false) const [creating, setCreating] = useState(false)
const [updating, setUpdating] = useState(false) const [updating, setUpdating] = useState(false)
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
const [showPassword, setShowPassword] = useState(false)
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: "", name: "",
slug: "", slug: "",
email: "", email: "",
password: "",
confirmPassword: "",
document: "",
phone: "",
website: "",
address: "",
description: "",
}) })
const [editFormData, setEditFormData] = useState({ const [editFormData, setEditFormData] = useState({
name: "", name: "",
@ -121,14 +153,35 @@ export default function AdminCompaniesPage() {
const handleCreate = async () => { const handleCreate = async () => {
try { try {
setCreating(true) setCreating(true)
await adminCompaniesApi.create(formData) // Strip non-digits for payload
toast.success("Company created successfully!") const payload = {
...formData,
document: formData.document.replace(/\D/g, ''),
phone: formData.phone.replace(/\D/g, ''),
}
await adminCompaniesApi.create(payload)
toast.success(t('admin.companies.success.created'))
setIsDialogOpen(false) setIsDialogOpen(false)
setFormData({ name: "", slug: "", email: "" }) setFormData({
name: "",
slug: "",
email: "",
password: "",
confirmPassword: "",
document: "",
phone: "",
website: "",
address: "",
description: "",
})
loadCompanies(1) // Reload first page loadCompanies(1) // Reload first page
} catch (error) { } catch (error: any) {
console.error("Error creating company:", error) console.error("Error creating company:", error)
if (error.message?.includes("already exists")) {
toast.error(t('admin.companies.error.emailExists', { defaultValue: "User with this email already exists" }))
} else {
toast.error("Failed to create company") toast.error("Failed to create company")
}
} finally { } finally {
setCreating(false) setCreating(false)
} }
@ -147,7 +200,7 @@ export default function AdminCompaniesPage() {
try { try {
await adminCompaniesApi.updateStatus(company.id, { [field]: newValue }) await adminCompaniesApi.updateStatus(company.id, { [field]: newValue })
toast.success(`Company ${field} updated`) toast.success(t('admin.companies.success.statusUpdated', { field }))
} catch (error) { } catch (error) {
toast.error(`Failed to update ${field}`) toast.error(`Failed to update ${field}`)
setCompanies(originalCompanies) setCompanies(originalCompanies)
@ -164,11 +217,11 @@ export default function AdminCompaniesPage() {
} }
const handleDelete = async (company: AdminCompany) => { const handleDelete = async (company: AdminCompany) => {
if (!window.confirm(`Are you sure you want to delete ${company.name}? This action cannot be undone.`)) return if (!window.confirm(t('admin.companies.deleteConfirm', { name: company.name }))) return
try { try {
await adminCompaniesApi.delete(company.id) await adminCompaniesApi.delete(company.id)
toast.success("Company deleted successfully") toast.success(t('admin.companies.success.deleted'))
setIsViewDialogOpen(false) setIsViewDialogOpen(false)
loadCompanies() loadCompanies()
} catch (error) { } catch (error) {
@ -218,7 +271,7 @@ export default function AdminCompaniesPage() {
description: editFormData.description, description: editFormData.description,
}) })
toast.success("Company updated successfully") toast.success(t('admin.companies.success.updated'))
setIsEditDialogOpen(false) setIsEditDialogOpen(false)
loadCompanies() loadCompanies()
} catch (error) { } catch (error) {
@ -240,29 +293,29 @@ export default function AdminCompaniesPage() {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold text-foreground">Company management</h1> <h1 className="text-3xl font-bold text-foreground">{t('admin.companies.title')}</h1>
<p className="text-muted-foreground mt-1">Manage all registered companies</p> <p className="text-muted-foreground mt-1">{t('admin.companies.subtitle')}</p>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button variant="outline" onClick={() => loadCompanies()} disabled={loading}> <Button variant="outline" onClick={() => loadCompanies()} disabled={loading}>
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? "animate-spin" : ""}`} /> <RefreshCw className={`h-4 w-4 mr-2 ${loading ? "animate-spin" : ""}`} />
Refresh {t('admin.companies.refresh')}
</Button> </Button>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button className="gap-2"> <Button className="gap-2">
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
New company {t('admin.companies.newCompany')}
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Create new company</DialogTitle> <DialogTitle>{t('admin.companies.create.title')}</DialogTitle>
<DialogDescription>Fill in the company details</DialogDescription> <DialogDescription>{t('admin.companies.create.subtitle')}</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="name">Company name</Label> <Label htmlFor="name">{t('admin.companies.create.name')}</Label>
<Input <Input
id="name" id="name"
value={formData.name} value={formData.name}
@ -273,34 +326,134 @@ export default function AdminCompaniesPage() {
slug: generateSlug(e.target.value), slug: generateSlug(e.target.value),
}) })
} }
placeholder="Company XYZ" placeholder={t('admin.companies.create.namePlaceholder')}
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="slug">Slug (URL)</Label> <Label htmlFor="slug">{t('admin.companies.create.slug')}</Label>
<Input <Input
id="slug" id="slug"
value={formData.slug} value={formData.slug}
onChange={(e) => setFormData({ ...formData, slug: e.target.value })} onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
placeholder="empresa-xyz" placeholder={t('admin.companies.create.slugPlaceholder')}
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="email">Email</Label> <Label htmlFor="document">{t('admin.companies.fields.document')}</Label>
<Input
id="document"
maxLength={18}
value={formData.document}
onChange={(e) => setFormData({ ...formData, document: formatCNPJ(e.target.value) })}
placeholder="CNPJ / Document"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="email">{t('admin.companies.create.email')}</Label>
<Input <Input
id="email" id="email"
type="email" type="email"
value={formData.email} value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })} onChange={(e) => setFormData({ ...formData, email: e.target.value })}
placeholder="hello@company.com" placeholder={t('admin.companies.create.emailPlaceholder')}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="password">{t('admin.companies.fields.password')}</Label>
<div className="relative">
<Input
id="password"
type={showPassword ? "text" : "password"}
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
placeholder="******"
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeOff className="h-4 w-4 text-muted-foreground" />
) : (
<Eye className="h-4 w-4 text-muted-foreground" />
)}
</Button>
</div>
</div>
<div className="grid gap-2">
<Label htmlFor="confirmPassword">{t('admin.companies.fields.confirmPassword')}</Label>
<div className="relative">
<Input
id="confirmPassword"
type={showConfirmPassword ? "text" : "password"}
value={formData.confirmPassword}
onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
placeholder="******"
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
>
{showConfirmPassword ? (
<EyeOff className="h-4 w-4 text-muted-foreground" />
) : (
<Eye className="h-4 w-4 text-muted-foreground" />
)}
</Button>
</div>
{formData.password !== formData.confirmPassword && formData.confirmPassword && (
<p className="text-xs text-red-500">{t('admin.companies.fields.passwordsDoNotMatch')}</p>
)}
</div>
<div className="grid gap-2">
<Label htmlFor="phone">{t('admin.companies.fields.phone')}</Label>
<Input
id="phone"
maxLength={15}
value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: formatPhone(e.target.value) })}
placeholder="+55 11 99999-9999"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="website">{t('admin.companies.fields.website')}</Label>
<Input
id="website"
value={formData.website}
onChange={(e) => setFormData({ ...formData, website: e.target.value })}
placeholder="https://..."
/>
</div>
<div className="grid gap-2">
<Label htmlFor="address">{t('admin.companies.fields.address')}</Label>
<Input
id="address"
value={formData.address}
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
placeholder="Address..."
/>
</div>
<div className="grid gap-2">
<Label htmlFor="description">{t('admin.companies.fields.description')}</Label>
<Textarea
id="description"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="Company description..."
/> />
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>Cancel</Button> <Button variant="outline" onClick={() => setIsDialogOpen(false)}>{t('admin.companies.create.cancel')}</Button>
<Button onClick={handleCreate} disabled={creating}> <Button onClick={handleCreate} disabled={creating}>
{creating && <Loader2 className="h-4 w-4 mr-2 animate-spin" />} {creating && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Create company {t('admin.companies.create.submit')}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
@ -312,25 +465,25 @@ export default function AdminCompaniesPage() {
<div className="grid gap-4 md:grid-cols-4"> <div className="grid gap-4 md:grid-cols-4">
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardDescription>Total companies</CardDescription> <CardDescription>{t('admin.companies.stats.total')}</CardDescription>
<CardTitle className="text-3xl">{totalCompanies}</CardTitle> <CardTitle className="text-3xl">{totalCompanies}</CardTitle>
</CardHeader> </CardHeader>
</Card> </Card>
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardDescription>Active companies</CardDescription> <CardDescription>{t('admin.companies.stats.active')}</CardDescription>
<CardTitle className="text-3xl">{companies.filter((c) => c.active).length}</CardTitle> <CardTitle className="text-3xl">{companies.filter((c) => c.active).length}</CardTitle>
</CardHeader> </CardHeader>
</Card> </Card>
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardDescription>Verified</CardDescription> <CardDescription>{t('admin.companies.stats.verified')}</CardDescription>
<CardTitle className="text-3xl">{companies.filter((c) => c.verified).length}</CardTitle> <CardTitle className="text-3xl">{companies.filter((c) => c.verified).length}</CardTitle>
</CardHeader> </CardHeader>
</Card> </Card>
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardDescription>Pending</CardDescription> <CardDescription>{t('admin.companies.stats.pending')}</CardDescription>
<CardTitle className="text-3xl">{companies.filter((c) => !c.verified).length}</CardTitle> <CardTitle className="text-3xl">{companies.filter((c) => !c.verified).length}</CardTitle>
</CardHeader> </CardHeader>
</Card> </Card>
@ -343,7 +496,7 @@ export default function AdminCompaniesPage() {
<div className="relative flex-1"> <div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input <Input
placeholder="Search companies by name or email..." placeholder={t('admin.companies.searchPlaceholder')}
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10" className="pl-10"
@ -364,19 +517,19 @@ export default function AdminCompaniesPage() {
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>Company</TableHead> <TableHead>{t('admin.companies.table.company')}</TableHead>
<TableHead>Email</TableHead> <TableHead>{t('admin.companies.table.email')}</TableHead>
<TableHead>Status</TableHead> <TableHead>{t('admin.companies.table.status')}</TableHead>
<TableHead>Verified</TableHead> <TableHead>{t('admin.companies.table.verified')}</TableHead>
<TableHead>Created</TableHead> <TableHead>{t('admin.companies.table.created')}</TableHead>
<TableHead className="text-right">Actions</TableHead> <TableHead className="text-right">{t('admin.companies.table.actions')}</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{filteredCompanies.length === 0 ? ( {filteredCompanies.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground py-8"> <TableCell colSpan={6} className="text-center text-muted-foreground py-8">
No companies found {t('admin.companies.table.empty')}
</TableCell> </TableCell>
</TableRow> </TableRow>
) : ( ) : (
@ -395,7 +548,7 @@ export default function AdminCompaniesPage() {
className="cursor-pointer hover:opacity-80" className="cursor-pointer hover:opacity-80"
onClick={() => toggleStatus(company, 'active')} onClick={() => toggleStatus(company, 'active')}
> >
{company.active ? "Active" : "Inactive"} {company.active ? t('admin.companies.fields.active') : t('admin.companies.fields.inactive')}
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell> <TableCell>
@ -414,9 +567,17 @@ export default function AdminCompaniesPage() {
{company.createdAt ? companyDateFormatter.format(new Date(company.createdAt)) : "-"} {company.createdAt ? companyDateFormatter.format(new Date(company.createdAt)) : "-"}
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
<Button variant="ghost" size="icon" onClick={() => handleView(company)}> <Button variant="ghost" size="icon" onClick={() => handleView(company)}>
<Eye className="h-4 w-4" /> <Eye className="h-4 w-4" />
</Button> </Button>
<Button variant="ghost" size="icon" onClick={() => handleEditClick(company)}>
<Pencil className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" className="text-destructive hover:text-destructive" onClick={() => handleDelete(company)}>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell> </TableCell>
</TableRow> </TableRow>
)) ))
@ -428,8 +589,12 @@ export default function AdminCompaniesPage() {
<div className="flex flex-wrap items-center justify-between gap-2 text-sm text-muted-foreground mt-4"> <div className="flex flex-wrap items-center justify-between gap-2 text-sm text-muted-foreground mt-4">
<span> <span>
{totalCompanies === 0 {totalCompanies === 0
? "No companies to display" ? t('admin.companies.table.empty')
: `Showing ${(page - 1) * limit + 1}-${Math.min(page * limit, totalCompanies)} of ${totalCompanies}`} : t('admin.companies.table.showing', {
from: (page - 1) * limit + 1,
to: Math.min(page * limit, totalCompanies),
total: totalCompanies
})}
</span> </span>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button
@ -464,17 +629,17 @@ export default function AdminCompaniesPage() {
<Building2 className="h-5 w-5" /> <Building2 className="h-5 w-5" />
{selectedCompany?.name} {selectedCompany?.name}
</DialogTitle> </DialogTitle>
<DialogDescription>Company details and information</DialogDescription> <DialogDescription>{t('admin.companies.details.subtitle')}</DialogDescription>
</DialogHeader> </DialogHeader>
{selectedCompany && ( {selectedCompany && (
<div className="space-y-6 py-4"> <div className="space-y-6 py-4">
{/* Status Badges */} {/* Status Badges */}
<div className="flex gap-2"> <div className="flex gap-2">
<Badge variant={selectedCompany.active ? "default" : "secondary"}> <Badge variant={selectedCompany.active ? "default" : "secondary"}>
{selectedCompany.active ? "Active" : "Inactive"} {selectedCompany.active ? t('admin.companies.fields.active') : t('admin.companies.fields.inactive')}
</Badge> </Badge>
<Badge variant={selectedCompany.verified ? "default" : "outline"}> <Badge variant={selectedCompany.verified ? "default" : "outline"}>
{selectedCompany.verified ? "Verified" : "Not Verified"} {selectedCompany.verified ? t('admin.companies.stats.verified') : "Not Verified"}
</Badge> </Badge>
{selectedCompany.type && ( {selectedCompany.type && (
<Badge variant="outline">{selectedCompany.type}</Badge> <Badge variant="outline">{selectedCompany.type}</Badge>
@ -484,19 +649,19 @@ export default function AdminCompaniesPage() {
{/* Basic Info */} {/* Basic Info */}
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<Label className="text-muted-foreground text-xs">Slug</Label> <Label className="text-muted-foreground text-xs">{t('admin.companies.create.slug')}</Label>
<p className="font-mono text-sm">{selectedCompany.slug}</p> <p className="font-mono text-sm">{selectedCompany.slug}</p>
</div> </div>
<div> <div>
<Label className="text-muted-foreground text-xs">Email</Label> <Label className="text-muted-foreground text-xs">{t('admin.companies.fields.email')}</Label>
<p className="text-sm">{selectedCompany.email || "-"}</p> <p className="text-sm">{selectedCompany.email || "-"}</p>
</div> </div>
<div> <div>
<Label className="text-muted-foreground text-xs">Phone</Label> <Label className="text-muted-foreground text-xs">{t('admin.companies.fields.phone')}</Label>
<p className="text-sm">{selectedCompany.phone || "-"}</p> <p className="text-sm">{selectedCompany.phone || "-"}</p>
</div> </div>
<div> <div>
<Label className="text-muted-foreground text-xs">Website</Label> <Label className="text-muted-foreground text-xs">{t('admin.companies.fields.website')}</Label>
<p className="text-sm"> <p className="text-sm">
{selectedCompany.website ? ( {selectedCompany.website ? (
<a <a
@ -513,11 +678,11 @@ export default function AdminCompaniesPage() {
</p> </p>
</div> </div>
<div> <div>
<Label className="text-muted-foreground text-xs">Document (CNPJ)</Label> <Label className="text-muted-foreground text-xs">{t('admin.companies.fields.document')}</Label>
<p className="text-sm font-mono">{selectedCompany.document || "-"}</p> <p className="text-sm font-mono">{selectedCompany.document || "-"}</p>
</div> </div>
<div> <div>
<Label className="text-muted-foreground text-xs">Address</Label> <Label className="text-muted-foreground text-xs">{t('admin.companies.fields.address')}</Label>
<p className="text-sm">{selectedCompany.address || "-"}</p> <p className="text-sm">{selectedCompany.address || "-"}</p>
</div> </div>
</div> </div>
@ -525,7 +690,7 @@ export default function AdminCompaniesPage() {
{/* Description */} {/* Description */}
{selectedCompany.description && ( {selectedCompany.description && (
<div> <div>
<Label className="text-muted-foreground text-xs">Description</Label> <Label className="text-muted-foreground text-xs">{t('admin.companies.fields.description')}</Label>
<div className="mt-1"> <div className="mt-1">
{formatDescription(selectedCompany.description)} {formatDescription(selectedCompany.description)}
</div> </div>
@ -535,7 +700,7 @@ export default function AdminCompaniesPage() {
{/* Timestamps */} {/* Timestamps */}
<div className="grid grid-cols-2 gap-4 pt-4 border-t"> <div className="grid grid-cols-2 gap-4 pt-4 border-t">
<div> <div>
<Label className="text-muted-foreground text-xs">Created At</Label> <Label className="text-muted-foreground text-xs">{t('admin.companies.fields.createdAt')}</Label>
<p className="text-sm"> <p className="text-sm">
{selectedCompany.createdAt {selectedCompany.createdAt
? companyDateFormatter.format(new Date(selectedCompany.createdAt)) ? companyDateFormatter.format(new Date(selectedCompany.createdAt))
@ -543,7 +708,7 @@ export default function AdminCompaniesPage() {
</p> </p>
</div> </div>
<div> <div>
<Label className="text-muted-foreground text-xs">Updated At</Label> <Label className="text-muted-foreground text-xs">{t('admin.companies.fields.updatedAt')}</Label>
<p className="text-sm"> <p className="text-sm">
{selectedCompany.updatedAt {selectedCompany.updatedAt
? companyDateFormatter.format(new Date(selectedCompany.updatedAt)) ? companyDateFormatter.format(new Date(selectedCompany.updatedAt))
@ -560,17 +725,17 @@ export default function AdminCompaniesPage() {
onClick={() => handleDelete(selectedCompany)} onClick={() => handleDelete(selectedCompany)}
> >
<Trash2 className="h-4 w-4 mr-2" /> <Trash2 className="h-4 w-4 mr-2" />
Delete {t('admin.companies.details.delete')}
</Button> </Button>
)} )}
<div className="flex gap-2"> <div className="flex gap-2">
<Button variant="outline" onClick={() => setIsViewDialogOpen(false)}> <Button variant="outline" onClick={() => setIsViewDialogOpen(false)}>
Close {t('admin.companies.details.close')}
</Button> </Button>
{selectedCompany && ( {selectedCompany && (
<Button onClick={() => handleEditClick(selectedCompany)}> <Button onClick={() => handleEditClick(selectedCompany)}>
<Pencil className="h-4 w-4 mr-2" /> <Pencil className="h-4 w-4 mr-2" />
Edit {t('admin.companies.details.edit')}
</Button> </Button>
)} )}
</div> </div>
@ -581,8 +746,8 @@ export default function AdminCompaniesPage() {
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}> <Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
<DialogContent className="max-w-md max-h-[90vh] overflow-y-auto"> <DialogContent className="max-w-md max-h-[90vh] overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle>Edit company</DialogTitle> <DialogTitle>{t('admin.companies.edit.title')}</DialogTitle>
<DialogDescription>Update company information</DialogDescription> <DialogDescription>{t('admin.companies.edit.subtitle')}</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
<div className="flex items-center gap-4 border p-4 rounded-md"> <div className="flex items-center gap-4 border p-4 rounded-md">
@ -592,7 +757,7 @@ export default function AdminCompaniesPage() {
onCheckedChange={(checked) => setEditFormData({ ...editFormData, active: checked })} onCheckedChange={(checked) => setEditFormData({ ...editFormData, active: checked })}
id="edit-active" id="edit-active"
/> />
<Label htmlFor="edit-active">Active</Label> <Label htmlFor="edit-active">{t('admin.companies.fields.active')}</Label>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Switch <Switch
@ -600,11 +765,11 @@ export default function AdminCompaniesPage() {
onCheckedChange={(checked) => setEditFormData({ ...editFormData, verified: checked })} onCheckedChange={(checked) => setEditFormData({ ...editFormData, verified: checked })}
id="edit-verified" id="edit-verified"
/> />
<Label htmlFor="edit-verified">Verified</Label> <Label htmlFor="edit-verified">{t('admin.companies.stats.verified')}</Label>
</div> </div>
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="edit-name">Company name</Label> <Label htmlFor="edit-name">{t('admin.companies.create.name')}</Label>
<Input <Input
id="edit-name" id="edit-name"
value={editFormData.name} value={editFormData.name}
@ -612,7 +777,7 @@ export default function AdminCompaniesPage() {
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="edit-slug">Slug</Label> <Label htmlFor="edit-slug">{t('admin.companies.create.slug')}</Label>
<Input <Input
id="edit-slug" id="edit-slug"
value={editFormData.slug} value={editFormData.slug}
@ -620,7 +785,7 @@ export default function AdminCompaniesPage() {
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="edit-email">Email</Label> <Label htmlFor="edit-email">{t('admin.companies.create.email')}</Label>
<Input <Input
id="edit-email" id="edit-email"
value={editFormData.email} value={editFormData.email}
@ -628,7 +793,7 @@ export default function AdminCompaniesPage() {
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="edit-phone">Phone</Label> <Label htmlFor="edit-phone">{t('admin.companies.fields.phone')}</Label>
<Input <Input
id="edit-phone" id="edit-phone"
value={editFormData.phone} value={editFormData.phone}
@ -636,7 +801,7 @@ export default function AdminCompaniesPage() {
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="edit-website">Website</Label> <Label htmlFor="edit-website">{t('admin.companies.fields.website')}</Label>
<Input <Input
id="edit-website" id="edit-website"
value={editFormData.website} value={editFormData.website}
@ -644,7 +809,7 @@ export default function AdminCompaniesPage() {
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="edit-document">Document</Label> <Label htmlFor="edit-document">{t('admin.companies.fields.document')}</Label>
<Input <Input
id="edit-document" id="edit-document"
value={editFormData.document} value={editFormData.document}
@ -652,7 +817,7 @@ export default function AdminCompaniesPage() {
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="edit-address">Address</Label> <Label htmlFor="edit-address">{t('admin.companies.fields.address')}</Label>
<Input <Input
id="edit-address" id="edit-address"
value={editFormData.address} value={editFormData.address}
@ -660,7 +825,7 @@ export default function AdminCompaniesPage() {
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="edit-description">Description</Label> <Label htmlFor="edit-description">{t('admin.companies.fields.description')}</Label>
<Input <Input
id="edit-description" id="edit-description"
value={editFormData.description} value={editFormData.description}
@ -669,10 +834,10 @@ export default function AdminCompaniesPage() {
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => setIsEditDialogOpen(false)}>Cancel</Button> <Button variant="outline" onClick={() => setIsEditDialogOpen(false)}>{t('admin.companies.create.cancel')}</Button>
<Button onClick={handleUpdate} disabled={updating}> <Button onClick={handleUpdate} disabled={updating}>
{updating && <Loader2 className="h-4 w-4 mr-2 animate-spin" />} {updating && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Save changes {t('admin.companies.edit.save')}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>

View file

@ -11,10 +11,12 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Separator } from "@/components/ui/separator" import { Separator } from "@/components/ui/separator"
import { ArrowLeft, Loader2, Building2, DollarSign, FileText, Briefcase, MapPin, Clock } from "lucide-react" import { ArrowLeft, Loader2, Building2, DollarSign, FileText, Briefcase, MapPin, Clock } from "lucide-react"
import { jobsApi, adminCompaniesApi, type CreateJobPayload, type AdminCompany } from "@/lib/api" import { jobsApi, adminCompaniesApi, type CreateJobPayload, type AdminCompany } from "@/lib/api"
import { useTranslation } from "@/lib/i18n"
import { toast } from "sonner" import { toast } from "sonner"
export default function NewJobPage() { export default function NewJobPage() {
const router = useRouter() const router = useRouter()
const { t } = useTranslation()
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
const [companies, setCompanies] = useState<AdminCompany[]>([]) const [companies, setCompanies] = useState<AdminCompany[]>([])
const [loadingCompanies, setLoadingCompanies] = useState(true) const [loadingCompanies, setLoadingCompanies] = useState(true)
@ -104,8 +106,8 @@ export default function NewJobPage() {
<ArrowLeft className="h-5 w-5" /> <ArrowLeft className="h-5 w-5" />
</Button> </Button>
<div> <div>
<h1 className="text-3xl font-bold">Post a job</h1> <h1 className="text-3xl font-bold">{t('admin.jobs.newJob')}</h1>
<p className="text-muted-foreground">Fill in the details below to create your job listing</p> <p className="text-muted-foreground">{t('admin.jobs.edit.subtitle')}</p>
</div> </div>
</div> </div>
@ -114,13 +116,13 @@ export default function NewJobPage() {
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<FileText className="h-5 w-5" /> <FileText className="h-5 w-5" />
Job Details {t('admin.jobs.details.title')}
</CardTitle> </CardTitle>
<CardDescription>Basic information about this position</CardDescription> <CardDescription>{t('admin.jobs.details.description')}</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="title">Job Title *</Label> <Label htmlFor="title">{t('admin.jobs.edit.jobTitle')} *</Label>
<Input <Input
id="title" id="title"
placeholder="e.g. Senior Software Engineer" placeholder="e.g. Senior Software Engineer"
@ -132,7 +134,7 @@ export default function NewJobPage() {
)} )}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="description">Job Description *</Label> <Label htmlFor="description">{t('admin.jobs.details.description')} *</Label>
<Textarea <Textarea
id="description" id="description"
placeholder="Describe the role, responsibilities, and requirements..." placeholder="Describe the role, responsibilities, and requirements..."
@ -147,7 +149,7 @@ export default function NewJobPage() {
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="location" className="flex items-center gap-1"> <Label htmlFor="location" className="flex items-center gap-1">
<MapPin className="h-4 w-4" /> <MapPin className="h-4 w-4" />
Location {t('admin.candidates_page.table.location')}
</Label> </Label>
<Input <Input
id="location" id="location"
@ -257,7 +259,7 @@ export default function NewJobPage() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-2"> <div className="space-y-2">
<Label>Company *</Label> <Label>{t('admin.jobs.table.company')} *</Label>
{loadingCompanies ? ( {loadingCompanies ? (
<div className="flex items-center gap-2 text-muted-foreground"> <div className="flex items-center gap-2 text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
@ -288,7 +290,7 @@ export default function NewJobPage() {
{/* Action Buttons */} {/* Action Buttons */}
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<Button variant="outline" onClick={() => router.back()}> <Button variant="outline" onClick={() => router.back()}>
Cancel {t('admin.jobs.edit.cancel')}
</Button> </Button>
<div className="flex gap-3"> <div className="flex gap-3">
<Button <Button
@ -317,7 +319,7 @@ export default function NewJobPage() {
) : ( ) : (
<> <>
<Briefcase className="h-4 w-4 mr-2" /> <Briefcase className="h-4 w-4 mr-2" />
Publish Job {t('admin.jobs.edit.save')}
</> </>
)} )}
</Button> </Button>

View file

@ -14,13 +14,11 @@ import {
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Plus, Search, Edit, Trash2, Eye, ChevronLeft, ChevronRight } from "lucide-react" import { Plus, Search, Edit, Trash2, Eye, ChevronLeft, ChevronRight } from "lucide-react"
import { adminJobsApi, adminCompaniesApi, jobsApi, type AdminJob, type AdminCompany } from "@/lib/api" import { adminJobsApi, adminCompaniesApi, jobsApi, type AdminJob, type AdminCompany } from "@/lib/api"
import { useTranslation } from "@/lib/i18n"
type AdminJobRow = { type AdminJobRow = {
id: string id: string
@ -38,16 +36,15 @@ type AdminJobRow = {
} }
export default function AdminJobsPage() { export default function AdminJobsPage() {
const { t } = useTranslation()
const [searchTerm, setSearchTerm] = useState("") const [searchTerm, setSearchTerm] = useState("")
const [jobs, setJobs] = useState<AdminJob[]>([]) const [jobs, setJobs] = useState<AdminJob[]>([])
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
const [isViewDialogOpen, setIsViewDialogOpen] = useState(false) const [isViewDialogOpen, setIsViewDialogOpen] = useState(false)
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
const [selectedJob, setSelectedJob] = useState<AdminJobRow | null>(null) const [selectedJob, setSelectedJob] = useState<AdminJobRow | null>(null)
const [editForm, setEditForm] = useState<{ title?: string }>({}) const [editForm, setEditForm] = useState<{ title?: string }>({})
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [errorMessage, setErrorMessage] = useState<string | null>(null) const [errorMessage, setErrorMessage] = useState<string | null>(null)
const [companies, setCompanies] = useState<AdminCompany[]>([])
// Pagination State // Pagination State
const [page, setPage] = useState(1) const [page, setPage] = useState(1)
@ -55,16 +52,6 @@ export default function AdminJobsPage() {
const [totalPages, setTotalPages] = useState(1) const [totalPages, setTotalPages] = useState(1)
const [totalJobs, setTotalJobs] = useState(0) const [totalJobs, setTotalJobs] = useState(0)
const [createForm, setCreateForm] = useState({
title: "",
company: "",
location: "",
type: "",
level: "",
salary: "",
description: "",
})
useEffect(() => { useEffect(() => {
const loadJobs = async () => { const loadJobs = async () => {
try { try {
@ -73,13 +60,6 @@ export default function AdminJobsPage() {
// Fetch with pagination // Fetch with pagination
const jobsData = await adminJobsApi.list({ limit, page }) const jobsData = await adminJobsApi.list({ limit, page })
setJobs(jobsData.data ?? []) setJobs(jobsData.data ?? [])
// Assuming metadata contains total/page info, or fallback if not available
// Need to check API response structure broadly, but assuming standard "meta" or similar
// For now, if no explicit meta, we rely on checking array length vs limit as a heuristic?
// Wait, adminJobsApi.list returns Promise<{ data: AdminJob[], meta?: ... }> ?
// Let's assume standard response for now. If API doesn't return total, we might need a separate count call or API update.
// Checking `adminJobsApi.list` later if issues arise. Assuming it returns `total` somewhere if needed.
// For now preventing errors:
if (jobsData.pagination) { if (jobsData.pagination) {
setTotalPages(Math.ceil((jobsData.pagination.total || 0) / limit)) setTotalPages(Math.ceil((jobsData.pagination.total || 0) / limit))
setTotalJobs(jobsData.pagination.total || 0) setTotalJobs(jobsData.pagination.total || 0)
@ -90,7 +70,7 @@ export default function AdminJobsPage() {
} catch (error) { } catch (error) {
console.error("Failed to load jobs:", error) console.error("Failed to load jobs:", error)
setErrorMessage("Unable to load jobs right now.") setErrorMessage(t('admin.jobs.table.error'))
setJobs([]) setJobs([])
} finally { } finally {
setIsLoading(false) setIsLoading(false)
@ -98,18 +78,7 @@ export default function AdminJobsPage() {
} }
loadJobs() loadJobs()
}, [page, limit, t])
// Load companies (keep this as looks like independent lookup)
const loadCompanies = async () => {
try {
const companiesData = await adminCompaniesApi.list(undefined, 1, 100)
setCompanies(companiesData.data ?? [])
} catch (error) {
console.error("[DEBUG] Failed to load companies:", error)
}
}
loadCompanies()
}, [page, limit]) // Reload when page changes
const jobRows = useMemo<AdminJobRow[]>( const jobRows = useMemo<AdminJobRow[]>(
() => () =>
@ -175,16 +144,14 @@ export default function AdminJobsPage() {
const handleDeleteJob = async (id: string) => { const handleDeleteJob = async (id: string) => {
console.log("[JOBS_PAGE] handleDeleteJob called with id:", id) console.log("[JOBS_PAGE] handleDeleteJob called with id:", id)
if (!confirm("Are you sure you want to delete this job?")) return if (!confirm(t('admin.jobs.deleteConfirm'))) return
try { try {
console.log("[JOBS_PAGE] Calling jobsApi.delete...")
await jobsApi.delete(id) await jobsApi.delete(id)
console.log("[JOBS_PAGE] Job deleted successfully, updating local state")
setJobs((prevJobs) => prevJobs.filter((job) => job.id !== id)) setJobs((prevJobs) => prevJobs.filter((job) => job.id !== id))
} catch (error) { } catch (error) {
console.error("[JOBS_PAGE] Failed to delete job:", error) console.error("[JOBS_PAGE] Failed to delete job:", error)
alert("Failed to delete job") alert(t('admin.jobs.deleteError'))
} }
} }
@ -192,21 +159,16 @@ export default function AdminJobsPage() {
const handleSaveEdit = async () => { const handleSaveEdit = async () => {
if (!selectedJob) return if (!selectedJob) return
console.log("[JOBS_PAGE] handleSaveEdit called for job:", selectedJob.id)
console.log("[JOBS_PAGE] Edit form data:", editForm)
try { try {
setIsLoading(true) setIsLoading(true)
console.log("[JOBS_PAGE] Calling jobsApi.update...") await jobsApi.update(selectedJob.id, editForm)
const updated = await jobsApi.update(selectedJob.id, editForm)
console.log("[JOBS_PAGE] Job updated successfully:", updated)
// Reload jobs to get fresh data // Reload jobs to get fresh data
const jobsData = await adminJobsApi.list({ limit: 10, page: 1 }) const jobsData = await adminJobsApi.list({ limit: 10, page: 1 })
setJobs(jobsData.data ?? []) setJobs(jobsData.data ?? [])
setIsEditDialogOpen(false) setIsEditDialogOpen(false)
} catch (error) { } catch (error) {
console.error("[JOBS_PAGE] Failed to update job:", error) console.error("[JOBS_PAGE] Failed to update job:", error)
alert("Failed to update job") alert(t('admin.jobs.updateError'))
} finally { } finally {
setIsLoading(false) setIsLoading(false)
} }
@ -218,7 +180,6 @@ export default function AdminJobsPage() {
} }
const handleNextPage = () => { const handleNextPage = () => {
// If we rely on generic "if array < limit then end" logic or strict meta total pages
if (page < totalPages) setPage(page + 1) if (page < totalPages) setPage(page + 1)
} }
@ -227,13 +188,13 @@ export default function AdminJobsPage() {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold text-foreground">Job management</h1> <h1 className="text-3xl font-bold text-foreground">{t('admin.jobs.title')}</h1>
<p className="text-muted-foreground mt-1">Manage all jobs posted on the platform</p> <p className="text-muted-foreground mt-1">{t('admin.jobs.subtitle')}</p>
</div> </div>
<Link href="/dashboard/jobs/new"> <Link href="/dashboard/jobs/new">
<Button className="gap-2"> <Button className="gap-2">
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
New job {t('admin.jobs.newJob')}
</Button> </Button>
</Link> </Link>
</div> </div>
@ -242,33 +203,30 @@ export default function AdminJobsPage() {
<Dialog open={isViewDialogOpen} onOpenChange={setIsViewDialogOpen}> <Dialog open={isViewDialogOpen} onOpenChange={setIsViewDialogOpen}>
<DialogContent className="max-w-2xl"> <DialogContent className="max-w-2xl">
<DialogHeader> <DialogHeader>
<DialogTitle>Job Details</DialogTitle> <DialogTitle>{t('admin.jobs.details.title')}</DialogTitle>
</DialogHeader> </DialogHeader>
{selectedJob && ( {selectedJob && (
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<Label className="text-muted-foreground">Title</Label> <Label className="text-muted-foreground">{t('admin.jobs.edit.jobTitle')}</Label>
<p className="font-medium">{selectedJob.title}</p> <p className="font-medium">{selectedJob.title}</p>
</div> </div>
<div> <div>
<Label className="text-muted-foreground">Company</Label> <Label className="text-muted-foreground">{t('admin.jobs.table.company')}</Label>
<p className="font-medium">{selectedJob.company}</p> <p className="font-medium">{selectedJob.company}</p>
</div> </div>
{/* Location and Type removed from table but kept in dialog if data exists, valid?
User asked to remove from "table" mainly. Keeping detail view intact is safer.
But wait, selectedJob still has them (empty strings). */}
<div> <div>
<Label className="text-muted-foreground">Status</Label> <Label className="text-muted-foreground">{t('admin.jobs.table.status')}</Label>
<p><Badge>{selectedJob.status}</Badge></p> <p><Badge>{selectedJob.status}</Badge></p>
</div> </div>
<div> <div>
<Label className="text-muted-foreground">Applications</Label> <Label className="text-muted-foreground">{t('admin.jobs.table.applications')}</Label>
<p className="font-medium">{selectedJob.applicationsCount}</p> <p className="font-medium">{selectedJob.applicationsCount}</p>
</div> </div>
</div> </div>
<div> <div>
<Label className="text-muted-foreground">Description</Label> <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"> <div className="mt-1 p-3 bg-muted rounded-md text-sm whitespace-pre-wrap max-h-60 overflow-y-auto">
{selectedJob.description} {selectedJob.description}
</div> </div>
@ -276,7 +234,7 @@ export default function AdminJobsPage() {
</div> </div>
)} )}
<DialogFooter> <DialogFooter>
<Button onClick={() => setIsViewDialogOpen(false)}>Close</Button> <Button onClick={() => setIsViewDialogOpen(false)}>{t('admin.jobs.details.close')}</Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@ -285,12 +243,12 @@ export default function AdminJobsPage() {
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}> <Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
<DialogContent className="max-w-2xl"> <DialogContent className="max-w-2xl">
<DialogHeader> <DialogHeader>
<DialogTitle>Edit Job</DialogTitle> <DialogTitle>{t('admin.jobs.edit.title')}</DialogTitle>
<DialogDescription>Update job details</DialogDescription> <DialogDescription>{t('admin.jobs.edit.subtitle')}</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="edit-title">Job title</Label> <Label htmlFor="edit-title">{t('admin.jobs.edit.jobTitle')}</Label>
<Input <Input
id="edit-title" id="edit-title"
value={editForm.title || ""} value={editForm.title || ""}
@ -299,8 +257,8 @@ export default function AdminJobsPage() {
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => setIsEditDialogOpen(false)}>Cancel</Button> <Button variant="outline" onClick={() => setIsEditDialogOpen(false)}>{t('admin.jobs.edit.cancel')}</Button>
<Button onClick={handleSaveEdit}>Save Changes</Button> <Button onClick={handleSaveEdit}>{t('admin.jobs.edit.save')}</Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@ -309,25 +267,25 @@ export default function AdminJobsPage() {
<div className="grid gap-4 md:grid-cols-4"> <div className="grid gap-4 md:grid-cols-4">
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardDescription>Total jobs</CardDescription> <CardDescription>{t('admin.jobs.stats.total')}</CardDescription>
<CardTitle className="text-3xl">{jobs.length}</CardTitle> <CardTitle className="text-3xl">{jobs.length}</CardTitle>
</CardHeader> </CardHeader>
</Card> </Card>
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardDescription>Active jobs</CardDescription> <CardDescription>{t('admin.jobs.stats.active')}</CardDescription>
<CardTitle className="text-3xl">{activeJobs}</CardTitle> <CardTitle className="text-3xl">{activeJobs}</CardTitle>
</CardHeader> </CardHeader>
</Card> </Card>
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardDescription>Applications</CardDescription> <CardDescription>{t('admin.jobs.stats.applications')}</CardDescription>
<CardTitle className="text-3xl">{totalApplications}</CardTitle> <CardTitle className="text-3xl">{totalApplications}</CardTitle>
</CardHeader> </CardHeader>
</Card> </Card>
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardDescription>Conversion rate</CardDescription> <CardDescription>{t('admin.jobs.stats.conversion')}</CardDescription>
<CardTitle className="text-3xl"> <CardTitle className="text-3xl">
{jobs.length > 0 ? Math.round((activeJobs / jobs.length) * 100) : 0}% {jobs.length > 0 ? Math.round((activeJobs / jobs.length) * 100) : 0}%
</CardTitle> </CardTitle>
@ -342,7 +300,7 @@ export default function AdminJobsPage() {
<div className="relative flex-1"> <div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input <Input
placeholder="Search jobs by title or company..." placeholder={t('admin.jobs.searchPlaceholder')}
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10" className="pl-10"
@ -354,19 +312,18 @@ export default function AdminJobsPage() {
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>Role</TableHead> <TableHead>{t('admin.jobs.table.role')}</TableHead>
<TableHead>Company</TableHead> <TableHead>{t('admin.jobs.table.company')}</TableHead>
{/* Removed Location and Type Headers */} <TableHead>{t('admin.jobs.table.applications')}</TableHead>
<TableHead>Applications</TableHead> <TableHead>{t('admin.jobs.table.status')}</TableHead>
<TableHead>Status</TableHead> <TableHead className="text-right">{t('admin.jobs.table.actions')}</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{isLoading ? ( {isLoading ? (
<TableRow> <TableRow>
<TableCell colSpan={5} className="text-center text-muted-foreground"> <TableCell colSpan={5} className="text-center text-muted-foreground">
Loading jobs... {t('admin.jobs.table.loading')}
</TableCell> </TableCell>
</TableRow> </TableRow>
) : errorMessage ? ( ) : errorMessage ? (
@ -378,7 +335,7 @@ export default function AdminJobsPage() {
) : filteredJobs.length === 0 ? ( ) : filteredJobs.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={5} className="text-center text-muted-foreground"> <TableCell colSpan={5} className="text-center text-muted-foreground">
No jobs found. {t('admin.jobs.table.empty')}
</TableCell> </TableCell>
</TableRow> </TableRow>
) : ( ) : (
@ -386,7 +343,6 @@ export default function AdminJobsPage() {
<TableRow key={job.id}> <TableRow key={job.id}>
<TableCell className="font-medium">{job.title}</TableCell> <TableCell className="font-medium">{job.title}</TableCell>
<TableCell>{job.company}</TableCell> <TableCell>{job.company}</TableCell>
{/* Removed Location and Type Cells */}
<TableCell>{job.applicationsCount ?? 0}</TableCell> <TableCell>{job.applicationsCount ?? 0}</TableCell>
<TableCell> <TableCell>
<Badge variant="default">{job.status ?? "Active"}</Badge> <Badge variant="default">{job.status ?? "Active"}</Badge>

View file

@ -227,11 +227,11 @@ export default function AdminUsersPage() {
const getRoleBadge = (role: string) => { const getRoleBadge = (role: string) => {
const labels: Record<string, string> = { const labels: Record<string, string> = {
superadmin: "Super Admin", superadmin: t('admin.users.roles.superadmin'),
admin: "Company Admin", admin: t('admin.users.roles.admin'),
recruiter: "Recruiter", recruiter: t('admin.users.roles.recruiter'),
candidate: "Candidate", candidate: t('admin.users.roles.candidate'),
company: "Company" company: t('admin.users.roles.admin') // Fallback for 'company' role legacy
} }
const colors: Record<string, "default" | "secondary" | "destructive" | "outline"> = { const colors: Record<string, "default" | "secondary" | "destructive" | "outline"> = {
superadmin: "destructive", superadmin: "destructive",
@ -295,15 +295,15 @@ export default function AdminUsersPage() {
type="password" type="password"
value={formData.password} value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })} onChange={(e) => setFormData({ ...formData, password: e.target.value })}
placeholder="Secure password" placeholder={t('admin.users.form.password')}
/> />
</div> </div>
{currentUser?.role === 'superadmin' && ( {currentUser?.role === 'superadmin' && (
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="company">Company</Label> <Label htmlFor="company">{t('admin.users.form.company')}</Label>
<Select value={formData.companyId} onValueChange={(v) => setFormData({ ...formData, companyId: v })}> <Select value={formData.companyId} onValueChange={(v) => setFormData({ ...formData, companyId: v })}>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select a company" /> <SelectValue placeholder={t('admin.users.form.select_company')} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{companies.map((company) => ( {companies.map((company) => (
@ -319,11 +319,11 @@ export default function AdminUsersPage() {
<Label htmlFor="status">{t('admin.users.table.status')}</Label> <Label htmlFor="status">{t('admin.users.table.status')}</Label>
<Select value={formData.status} onValueChange={(v) => setFormData({ ...formData, status: v })}> <Select value={formData.status} onValueChange={(v) => setFormData({ ...formData, status: v })}>
<SelectTrigger> <SelectTrigger>
<SelectValue /> <SelectValue placeholder={t('admin.users.form.status_placeholder')} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="active">Active</SelectItem> <SelectItem value="active">{t('admin.users.statuses.active')}</SelectItem>
<SelectItem value="inactive">Inactive</SelectItem> <SelectItem value="inactive">{t('admin.users.statuses.inactive')}</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@ -334,10 +334,10 @@ export default function AdminUsersPage() {
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="superadmin">Super Admin</SelectItem> <SelectItem value="superadmin">{t('admin.users.roles.superadmin')}</SelectItem>
<SelectItem value="admin">Company admin</SelectItem> <SelectItem value="admin">{t('admin.users.roles.admin')}</SelectItem>
<SelectItem value="recruiter">Recruiter</SelectItem> <SelectItem value="recruiter">{t('admin.users.roles.recruiter')}</SelectItem>
<SelectItem value="candidate">Candidate</SelectItem> <SelectItem value="candidate">{t('admin.users.roles.candidate')}</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@ -385,7 +385,7 @@ export default function AdminUsersPage() {
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="edit-role">Role</Label> <Label htmlFor="edit-role">{t('admin.users.table.role')}</Label>
<Select <Select
value={editFormData.role} value={editFormData.role}
onValueChange={(v) => setEditFormData({ ...editFormData, role: v })} onValueChange={(v) => setEditFormData({ ...editFormData, role: v })}
@ -395,15 +395,15 @@ export default function AdminUsersPage() {
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="superadmin">Super Admin</SelectItem> <SelectItem value="superadmin">{t('admin.users.roles.superadmin')}</SelectItem>
<SelectItem value="admin">Company admin</SelectItem> <SelectItem value="admin">{t('admin.users.roles.admin')}</SelectItem>
<SelectItem value="recruiter">Recruiter</SelectItem> <SelectItem value="recruiter">{t('admin.users.roles.recruiter')}</SelectItem>
<SelectItem value="candidate">Candidate</SelectItem> <SelectItem value="candidate">{t('admin.users.roles.candidate')}</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="edit-status">Status</Label> <Label htmlFor="edit-status">{t('admin.users.table.status')}</Label>
<Select <Select
value={editFormData.status} value={editFormData.status}
onValueChange={(v) => setEditFormData({ ...editFormData, status: v })} onValueChange={(v) => setEditFormData({ ...editFormData, status: v })}
@ -413,8 +413,8 @@ export default function AdminUsersPage() {
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="active">Active</SelectItem> <SelectItem value="active">{t('admin.users.statuses.active')}</SelectItem>
<SelectItem value="inactive">Inactive</SelectItem> <SelectItem value="inactive">{t('admin.users.statuses.inactive')}</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>

View file

@ -907,6 +907,23 @@
"delete_success": "User deleted!", "delete_success": "User deleted!",
"delete_error": "Failed to delete user", "delete_error": "Failed to delete user",
"load_error": "Failed to load users" "load_error": "Failed to load users"
},
"form": {
"password": "Secure password",
"company": "Company",
"select_company": "Select a company",
"role_placeholder": "Select a role",
"status_placeholder": "Select status"
},
"roles": {
"superadmin": "Super Admin",
"admin": "Company Admin",
"recruiter": "Recruiter",
"candidate": "Candidate"
},
"statuses": {
"active": "Active",
"inactive": "Inactive"
} }
}, },
"candidates_page": { "candidates_page": {
@ -949,6 +966,114 @@
"hired": "Hired", "hired": "Hired",
"rejected": "Rejected" "rejected": "Rejected"
} }
},
"jobs": {
"title": "Job management",
"subtitle": "Manage all jobs posted on the platform",
"newJob": "New job",
"stats": {
"total": "Total jobs",
"active": "Active jobs",
"applications": "Applications",
"conversion": "Conversion rate"
},
"searchPlaceholder": "Search jobs by title or company...",
"table": {
"role": "Role",
"company": "Company",
"applications": "Applications",
"status": "Status",
"actions": "Actions",
"empty": "No jobs found.",
"loading": "Loading jobs...",
"error": "Unable to load jobs right now."
},
"details": {
"title": "Job Details",
"description": "Job description",
"close": "Close"
},
"edit": {
"title": "Edit Job",
"subtitle": "Update job details",
"jobTitle": "Job title",
"cancel": "Cancel",
"save": "Save Changes"
},
"deleteConfirm": "Are you sure you want to delete this job?",
"deleteError": "Failed to delete job",
"updateError": "Failed to update job"
},
"companies": {
"title": "Company management",
"subtitle": "Manage all registered companies",
"newCompany": "New company",
"refresh": "Refresh",
"stats": {
"total": "Total companies",
"active": "Active companies",
"verified": "Verified",
"pending": "Pending"
},
"create": {
"title": "Create new company",
"subtitle": "Fill in the company details",
"name": "Company name",
"namePlaceholder": "Company XYZ",
"slug": "Slug (URL)",
"slugPlaceholder": "company-xyz",
"email": "Email",
"emailPlaceholder": "hello@company.com",
"cancel": "Cancel",
"submit": "Create company"
},
"edit": {
"title": "Edit company",
"subtitle": "Update company information",
"save": "Save changes"
},
"details": {
"title": "Company details",
"subtitle": "Company details and information",
"close": "Close",
"delete": "Delete",
"edit": "Edit"
},
"table": {
"company": "Company",
"email": "Email",
"status": "Status",
"verified": "Verified",
"created": "Created",
"actions": "Actions",
"empty": "No companies found",
"showing": "Showing {{from}}-{{to}} of {{total}}"
},
"searchPlaceholder": "Search companies by name or email...",
"deleteConfirm": "Are you sure you want to delete {{name}}? This action cannot be undone.",
"success": {
"created": "Company created successfully!",
"deleted": "Company deleted successfully",
"updated": "Company updated successfully",
"statusUpdated": "Company {{field}} updated"
},
"fields": {
"active": "Active",
"inactive": "Inactive",
"address": "Address",
"phone": "Phone",
"email": "Email",
"website": "Website",
"document": "Document (CNPJ)",
"description": "Description",
"password": "Password",
"confirmPassword": "Confirm Password",
"showPassword": "Show password",
"hidePassword": "Hide password",
"passwordsDoNotMatch": "Passwords do not match",
"createdAt": "Created At",
"updatedAt": "Updated At"
}
} }
}, },
"company": { "company": {

View file

@ -967,6 +967,23 @@
"delete_success": "Usuário excluído!", "delete_success": "Usuário excluído!",
"delete_error": "Falha ao excluir usuário", "delete_error": "Falha ao excluir usuário",
"load_error": "Falha ao carregar usuários" "load_error": "Falha ao carregar usuários"
},
"form": {
"password": "Senha segura",
"company": "Empresa",
"select_company": "Selecione uma empresa",
"role_placeholder": "Selecione uma função",
"status_placeholder": "Selecione o status"
},
"roles": {
"superadmin": "Super Admin",
"admin": "Admin da Empresa",
"recruiter": "Recrutador",
"candidate": "Candidato"
},
"statuses": {
"active": "Ativo",
"inactive": "Inativo"
} }
}, },
"candidates_page": { "candidates_page": {
@ -1009,6 +1026,114 @@
"hired": "Contratado", "hired": "Contratado",
"rejected": "Rejeitado" "rejected": "Rejeitado"
} }
},
"jobs": {
"title": "Gerenciamento de Vagas",
"subtitle": "Gerencie todas as vagas publicadas na plataforma",
"newJob": "Nova vaga",
"stats": {
"total": "Total de vagas",
"active": "Vagas ativas",
"applications": "Candidaturas",
"conversion": "Taxa de conversão"
},
"searchPlaceholder": "Buscar vagas por título ou empresa...",
"table": {
"role": "Cargo",
"company": "Empresa",
"applications": "Candidaturas",
"status": "Status",
"actions": "Ações",
"empty": "Nenhuma vaga encontrada.",
"loading": "Carregando vagas...",
"error": "Não foi possível carregar as vagas."
},
"details": {
"title": "Detalhes da Vaga",
"description": "Descrição da vaga",
"close": "Fechar"
},
"edit": {
"title": "Editar Vaga",
"subtitle": "Atualizar detalhes da vaga",
"jobTitle": "Título da vaga",
"cancel": "Cancelar",
"save": "Salvar Alterações"
},
"deleteConfirm": "Tem certeza que deseja excluir esta vaga?",
"deleteError": "Falha ao excluir vaga",
"updateError": "Falha ao atualizar vaga"
},
"companies": {
"title": "Gerenciamento de Empresas",
"subtitle": "Gerencie todas as empresas registradas",
"newCompany": "Nova empresa",
"refresh": "Atualizar",
"stats": {
"total": "Total de empresas",
"active": "Empresas ativas",
"verified": "Verificadas",
"pending": "Pendentes"
},
"create": {
"title": "Criar nova empresa",
"subtitle": "Preencha os dados da empresa",
"name": "Nome da empresa",
"namePlaceholder": "Empresa XYZ",
"slug": "Slug (URL)",
"slugPlaceholder": "empresa-xyz",
"email": "Email",
"emailPlaceholder": "ola@empresa.com",
"cancel": "Cancelar",
"submit": "Criar empresa"
},
"edit": {
"title": "Editar empresa",
"subtitle": "Atualizar informações da empresa",
"save": "Salvar alterações"
},
"details": {
"title": "Detalhes da empresa",
"subtitle": "Informações e detalhes da empresa",
"close": "Fechar",
"delete": "Excluir",
"edit": "Editar"
},
"table": {
"company": "Empresa",
"email": "Email",
"status": "Status",
"verified": "Verificado",
"created": "Criado em",
"actions": "Ações",
"empty": "Nenhuma empresa encontrada",
"showing": "Exibindo {{from}}-{{to}} de {{total}}"
},
"searchPlaceholder": "Buscar empresas por nome ou email...",
"deleteConfirm": "Tem certeza que deseja excluir {{name}}? Esta ação não pode ser desfeita.",
"success": {
"created": "Empresa criada com sucesso!",
"deleted": "Empresa excluída com sucesso",
"updated": "Empresa atualizada com sucesso",
"statusUpdated": "Empresa {{field}} atualizada"
},
"fields": {
"active": "Ativo",
"inactive": "Inativo",
"address": "Endereço",
"phone": "Telefone",
"email": "Email",
"website": "Site",
"document": "Documento (CNPJ)",
"description": "Descrição",
"password": "Senha",
"confirmPassword": "Confirmar Senha",
"showPassword": "Mostrar senha",
"hidePassword": "Ocultar senha",
"passwordsDoNotMatch": "Senhas não conferem",
"createdAt": "Criado em",
"updatedAt": "Atualizado em"
}
} }
}, },
"company": { "company": {