Merge pull request #41 from rede5/task5
feat: melhorias na gestão de empresas e correções de persistência Backend: Correção na persistência do email da empresa (CreateCompanyUseCase) e suporte a exclusão em cascata (Cascade Delete) para evitar erro 500. Backend: Adicionado suporte completo para Phone, Website, Address, Description e Slug na criação. Backend: Correção crítica no JobService para ocultar nome de candidatos na listagem de vagas. Frontend: Adição da coluna 'Email' na listagem de empresas e padronização dos ícones de ação. Frontend: Inclusão de novas chaves de tradução (i18n) e melhorias no modal de criação.
This commit is contained in:
commit
a35f06c5de
15 changed files with 700 additions and 209 deletions
|
|
@ -178,6 +178,10 @@ func (h *CoreHandlers) CreateCompany(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
resp, err := h.createCompanyUC.Execute(r.Context(), req)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,19 @@ import "time"
|
|||
|
||||
// Company represents a Tenant in the system.
|
||||
type Company struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Document *string `json:"document,omitempty"` // CNPJ, EIN, VAT
|
||||
Contact *string `json:"contact,omitempty"`
|
||||
Status string `json:"status"` // "ACTIVE", "INACTIVE"
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Document *string `json:"document,omitempty"` // CNPJ, EIN, VAT
|
||||
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"
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// NewCompany creates a new Company instance with defaults.
|
||||
|
|
@ -18,6 +24,8 @@ func NewCompany(id, name string, document, contact *string) *Company {
|
|||
return &Company{
|
||||
ID: id,
|
||||
Name: name,
|
||||
Slug: name, // Basic slug, repo might refine
|
||||
Type: "COMPANY",
|
||||
Document: document,
|
||||
Contact: contact,
|
||||
Status: "ACTIVE",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ type CreateCompanyRequest struct {
|
|||
Password string `json:"password"`
|
||||
Phone string `json:"phone"`
|
||||
Website *string `json:"website,omitempty"`
|
||||
Address *string `json:"address,omitempty"`
|
||||
EmployeeCount *string `json:"employeeCount,omitempty"`
|
||||
FoundedYear *int `json:"foundedYear,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ func (uc *RegisterCandidateUseCase) Execute(ctx context.Context, input dto.Regis
|
|||
nil, // No document for candidates
|
||||
nil, // No contact - will use user's contact info
|
||||
)
|
||||
candidateCompany.Type = "CANDIDATE_WORKSPACE"
|
||||
|
||||
savedCompany, err := uc.companyRepo.Save(ctx, candidateCompany)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package tenant
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/rede5/gohorsejobs/backend/internal/core/domain/entity"
|
||||
"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.
|
||||
// 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)
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. Create Admin User
|
||||
// We need a password for the admin. Do we generate one? Or did input provide?
|
||||
// input.AdminEmail is present. But no password. I'll generic default or ask to send email.
|
||||
// For simplicity, let's assume a default password "ChangeMe123!" hash it.
|
||||
hashedPassword, _ := uc.authService.HashPassword("ChangeMe123!")
|
||||
pwd := input.Password
|
||||
if pwd == "" {
|
||||
pwd = "ChangeMe123!"
|
||||
}
|
||||
hashedPassword, _ := uc.authService.HashPassword(pwd)
|
||||
|
||||
adminUser := entity.NewUser("", savedCompany.ID, "Admin", input.AdminEmail)
|
||||
adminUser.PasswordHash = hashedPassword
|
||||
adminUser.AssignRole(entity.Role{Name: "ADMIN"})
|
||||
adminUser.AssignRole(entity.Role{Name: entity.RoleAdmin})
|
||||
|
||||
_, err = uc.userRepo.Save(ctx, adminUser)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -70,4 +70,5 @@ type User struct {
|
|||
AvatarUrl *string `json:"avatarUrl,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
CompanyID *string `json:"companyId,omitempty"`
|
||||
Roles []string `json:"roles,omitempty"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,21 +20,30 @@ func NewCompanyRepository(db *sql.DB) *CompanyRepository {
|
|||
func (r *CompanyRepository) Save(ctx context.Context, company *entity.Company) (*entity.Company, error) {
|
||||
// companies table uses UUID id, DB generates it
|
||||
query := `
|
||||
INSERT INTO companies (name, slug, type, document, email, description, verified, active, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
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, $11, $12, $13)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
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
|
||||
err := r.db.QueryRowContext(ctx, query,
|
||||
company.Name,
|
||||
slug,
|
||||
"company",
|
||||
company.Type,
|
||||
company.Document,
|
||||
company.Contact, // mapped to email
|
||||
"{}", // description as JSON
|
||||
true, // verified
|
||||
company.Contact, // email
|
||||
company.Phone,
|
||||
company.Website,
|
||||
company.Address,
|
||||
company.Description,
|
||||
true, // verified
|
||||
company.Status == "ACTIVE",
|
||||
company.CreatedAt,
|
||||
company.UpdatedAt,
|
||||
|
|
|
|||
|
|
@ -25,10 +25,11 @@ func (s *AdminService) ListCompanies(ctx context.Context, verified *bool, page,
|
|||
offset := (page - 1) * limit
|
||||
|
||||
// Count Total
|
||||
countQuery := `SELECT COUNT(*) FROM companies`
|
||||
// Count Total
|
||||
countQuery := `SELECT COUNT(*) FROM companies WHERE type != 'CANDIDATE_WORKSPACE'`
|
||||
var countArgs []interface{}
|
||||
if verified != nil {
|
||||
countQuery += " WHERE verified = $1"
|
||||
countQuery += " AND verified = $1"
|
||||
countArgs = append(countArgs, *verified)
|
||||
}
|
||||
var total int
|
||||
|
|
@ -40,11 +41,12 @@ func (s *AdminService) ListCompanies(ctx context.Context, verified *bool, page,
|
|||
baseQuery := `
|
||||
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
|
||||
WHERE type != 'CANDIDATE_WORKSPACE'
|
||||
`
|
||||
|
||||
var args []interface{}
|
||||
if verified != nil {
|
||||
baseQuery += " WHERE verified = $1"
|
||||
baseQuery += " AND verified = $1"
|
||||
args = append(args, *verified)
|
||||
}
|
||||
|
||||
|
|
@ -610,9 +612,43 @@ func (s *AdminService) GetUser(ctx context.Context, id string) (*dto.User, error
|
|||
if avatarURL.Valid {
|
||||
u.AvatarUrl = &avatarURL.String
|
||||
}
|
||||
|
||||
// Fetch roles
|
||||
roles, err := s.getUserRoles(ctx, u.ID)
|
||||
if err == nil {
|
||||
u.Roles = roles
|
||||
}
|
||||
|
||||
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
|
||||
func (s *AdminService) GetCompanyByUserID(ctx context.Context, userID string) (*models.Company, error) {
|
||||
// First, try to find company where this user is admin
|
||||
|
|
@ -728,9 +764,28 @@ func (s *AdminService) DeleteCompany(ctx context.Context, id string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Delete
|
||||
_, err = s.DB.ExecContext(ctx, `DELETE FROM companies WHERE id=$1`, id)
|
||||
return err
|
||||
tx, err := s.DB.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
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()
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -79,7 +79,10 @@ func (s *JobService) GetJobs(filter dto.JobFilterQuery) ([]models.JobWithCompany
|
|||
SELECT
|
||||
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,
|
||||
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,
|
||||
(SELECT COUNT(*) FROM applications a WHERE a.job_id = j.id) as applications_count
|
||||
FROM jobs j
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useEffect, useState } from "react"
|
|||
import { useRouter } from "next/navigation"
|
||||
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"
|
||||
|
|
@ -17,12 +18,13 @@ import {
|
|||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
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 { adminCompaniesApi, type AdminCompany } from "@/lib/api"
|
||||
import { getCurrentUser, isAdminUser } from "@/lib/auth"
|
||||
import { toast } from "sonner"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { useTranslation } from "@/lib/i18n"
|
||||
|
||||
const companyDateFormatter = new Intl.DateTimeFormat("en-US", {
|
||||
dateStyle: "medium",
|
||||
|
|
@ -56,7 +58,28 @@ const formatDescription = (description: string | undefined) => {
|
|||
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() {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const [companies, setCompanies] = useState<AdminCompany[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
|
@ -69,10 +92,19 @@ export default function AdminCompaniesPage() {
|
|||
const [creating, setCreating] = useState(false)
|
||||
const [updating, setUpdating] = useState(false)
|
||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
slug: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
document: "",
|
||||
phone: "",
|
||||
website: "",
|
||||
address: "",
|
||||
description: "",
|
||||
})
|
||||
const [editFormData, setEditFormData] = useState({
|
||||
name: "",
|
||||
|
|
@ -121,14 +153,35 @@ export default function AdminCompaniesPage() {
|
|||
const handleCreate = async () => {
|
||||
try {
|
||||
setCreating(true)
|
||||
await adminCompaniesApi.create(formData)
|
||||
toast.success("Company created successfully!")
|
||||
// Strip non-digits for payload
|
||||
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)
|
||||
setFormData({ name: "", slug: "", email: "" })
|
||||
setFormData({
|
||||
name: "",
|
||||
slug: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
document: "",
|
||||
phone: "",
|
||||
website: "",
|
||||
address: "",
|
||||
description: "",
|
||||
})
|
||||
loadCompanies(1) // Reload first page
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error("Error creating company:", error)
|
||||
toast.error("Failed to create company")
|
||||
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")
|
||||
}
|
||||
} finally {
|
||||
setCreating(false)
|
||||
}
|
||||
|
|
@ -147,7 +200,7 @@ export default function AdminCompaniesPage() {
|
|||
|
||||
try {
|
||||
await adminCompaniesApi.updateStatus(company.id, { [field]: newValue })
|
||||
toast.success(`Company ${field} updated`)
|
||||
toast.success(t('admin.companies.success.statusUpdated', { field }))
|
||||
} catch (error) {
|
||||
toast.error(`Failed to update ${field}`)
|
||||
setCompanies(originalCompanies)
|
||||
|
|
@ -164,11 +217,11 @@ export default function AdminCompaniesPage() {
|
|||
}
|
||||
|
||||
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 {
|
||||
await adminCompaniesApi.delete(company.id)
|
||||
toast.success("Company deleted successfully")
|
||||
toast.success(t('admin.companies.success.deleted'))
|
||||
setIsViewDialogOpen(false)
|
||||
loadCompanies()
|
||||
} catch (error) {
|
||||
|
|
@ -218,7 +271,7 @@ export default function AdminCompaniesPage() {
|
|||
description: editFormData.description,
|
||||
})
|
||||
|
||||
toast.success("Company updated successfully")
|
||||
toast.success(t('admin.companies.success.updated'))
|
||||
setIsEditDialogOpen(false)
|
||||
loadCompanies()
|
||||
} catch (error) {
|
||||
|
|
@ -240,29 +293,29 @@ export default function AdminCompaniesPage() {
|
|||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-foreground">Company management</h1>
|
||||
<p className="text-muted-foreground mt-1">Manage all registered companies</p>
|
||||
<h1 className="text-3xl font-bold text-foreground">{t('admin.companies.title')}</h1>
|
||||
<p className="text-muted-foreground mt-1">{t('admin.companies.subtitle')}</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => loadCompanies()} disabled={loading}>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? "animate-spin" : ""}`} />
|
||||
Refresh
|
||||
{t('admin.companies.refresh')}
|
||||
</Button>
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
New company
|
||||
{t('admin.companies.newCompany')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create new company</DialogTitle>
|
||||
<DialogDescription>Fill in the company details</DialogDescription>
|
||||
<DialogTitle>{t('admin.companies.create.title')}</DialogTitle>
|
||||
<DialogDescription>{t('admin.companies.create.subtitle')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">Company name</Label>
|
||||
<Label htmlFor="name">{t('admin.companies.create.name')}</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={formData.name}
|
||||
|
|
@ -273,34 +326,134 @@ export default function AdminCompaniesPage() {
|
|||
slug: generateSlug(e.target.value),
|
||||
})
|
||||
}
|
||||
placeholder="Company XYZ"
|
||||
placeholder={t('admin.companies.create.namePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="slug">Slug (URL)</Label>
|
||||
<Label htmlFor="slug">{t('admin.companies.create.slug')}</Label>
|
||||
<Input
|
||||
id="slug"
|
||||
value={formData.slug}
|
||||
onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
|
||||
placeholder="empresa-xyz"
|
||||
placeholder={t('admin.companies.create.slugPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<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
|
||||
id="email"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
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>
|
||||
<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}>
|
||||
{creating && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||||
Create company
|
||||
{t('admin.companies.create.submit')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
|
@ -312,25 +465,25 @@ export default function AdminCompaniesPage() {
|
|||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardDescription>Total companies</CardDescription>
|
||||
<CardDescription>{t('admin.companies.stats.total')}</CardDescription>
|
||||
<CardTitle className="text-3xl">{totalCompanies}</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<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>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<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>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<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>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
|
@ -343,7 +496,7 @@ export default function AdminCompaniesPage() {
|
|||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search companies by name or email..."
|
||||
placeholder={t('admin.companies.searchPlaceholder')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
|
|
@ -364,19 +517,19 @@ export default function AdminCompaniesPage() {
|
|||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Company</TableHead>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Verified</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
<TableHead>{t('admin.companies.table.company')}</TableHead>
|
||||
<TableHead>{t('admin.companies.table.email')}</TableHead>
|
||||
<TableHead>{t('admin.companies.table.status')}</TableHead>
|
||||
<TableHead>{t('admin.companies.table.verified')}</TableHead>
|
||||
<TableHead>{t('admin.companies.table.created')}</TableHead>
|
||||
<TableHead className="text-right">{t('admin.companies.table.actions')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredCompanies.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center text-muted-foreground py-8">
|
||||
No companies found
|
||||
{t('admin.companies.table.empty')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
|
|
@ -395,7 +548,7 @@ export default function AdminCompaniesPage() {
|
|||
className="cursor-pointer hover:opacity-80"
|
||||
onClick={() => toggleStatus(company, 'active')}
|
||||
>
|
||||
{company.active ? "Active" : "Inactive"}
|
||||
{company.active ? t('admin.companies.fields.active') : t('admin.companies.fields.inactive')}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
|
|
@ -414,9 +567,17 @@ export default function AdminCompaniesPage() {
|
|||
{company.createdAt ? companyDateFormatter.format(new Date(company.createdAt)) : "-"}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button variant="ghost" size="icon" onClick={() => handleView(company)}>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button variant="ghost" size="icon" onClick={() => handleView(company)}>
|
||||
<Eye className="h-4 w-4" />
|
||||
</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>
|
||||
</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">
|
||||
<span>
|
||||
{totalCompanies === 0
|
||||
? "No companies to display"
|
||||
: `Showing ${(page - 1) * limit + 1}-${Math.min(page * limit, totalCompanies)} of ${totalCompanies}`}
|
||||
? t('admin.companies.table.empty')
|
||||
: t('admin.companies.table.showing', {
|
||||
from: (page - 1) * limit + 1,
|
||||
to: Math.min(page * limit, totalCompanies),
|
||||
total: totalCompanies
|
||||
})}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
|
|
@ -464,17 +629,17 @@ export default function AdminCompaniesPage() {
|
|||
<Building2 className="h-5 w-5" />
|
||||
{selectedCompany?.name}
|
||||
</DialogTitle>
|
||||
<DialogDescription>Company details and information</DialogDescription>
|
||||
<DialogDescription>{t('admin.companies.details.subtitle')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
{selectedCompany && (
|
||||
<div className="space-y-6 py-4">
|
||||
{/* Status Badges */}
|
||||
<div className="flex gap-2">
|
||||
<Badge variant={selectedCompany.active ? "default" : "secondary"}>
|
||||
{selectedCompany.active ? "Active" : "Inactive"}
|
||||
{selectedCompany.active ? t('admin.companies.fields.active') : t('admin.companies.fields.inactive')}
|
||||
</Badge>
|
||||
<Badge variant={selectedCompany.verified ? "default" : "outline"}>
|
||||
{selectedCompany.verified ? "Verified" : "Not Verified"}
|
||||
{selectedCompany.verified ? t('admin.companies.stats.verified') : "Not Verified"}
|
||||
</Badge>
|
||||
{selectedCompany.type && (
|
||||
<Badge variant="outline">{selectedCompany.type}</Badge>
|
||||
|
|
@ -484,19 +649,19 @@ export default function AdminCompaniesPage() {
|
|||
{/* Basic Info */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</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">
|
||||
{selectedCompany.website ? (
|
||||
<a
|
||||
|
|
@ -513,11 +678,11 @@ export default function AdminCompaniesPage() {
|
|||
</p>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -525,7 +690,7 @@ export default function AdminCompaniesPage() {
|
|||
{/* Description */}
|
||||
{selectedCompany.description && (
|
||||
<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">
|
||||
{formatDescription(selectedCompany.description)}
|
||||
</div>
|
||||
|
|
@ -535,7 +700,7 @@ export default function AdminCompaniesPage() {
|
|||
{/* Timestamps */}
|
||||
<div className="grid grid-cols-2 gap-4 pt-4 border-t">
|
||||
<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">
|
||||
{selectedCompany.createdAt
|
||||
? companyDateFormatter.format(new Date(selectedCompany.createdAt))
|
||||
|
|
@ -543,7 +708,7 @@ export default function AdminCompaniesPage() {
|
|||
</p>
|
||||
</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">
|
||||
{selectedCompany.updatedAt
|
||||
? companyDateFormatter.format(new Date(selectedCompany.updatedAt))
|
||||
|
|
@ -560,17 +725,17 @@ export default function AdminCompaniesPage() {
|
|||
onClick={() => handleDelete(selectedCompany)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Delete
|
||||
{t('admin.companies.details.delete')}
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => setIsViewDialogOpen(false)}>
|
||||
Close
|
||||
{t('admin.companies.details.close')}
|
||||
</Button>
|
||||
{selectedCompany && (
|
||||
<Button onClick={() => handleEditClick(selectedCompany)}>
|
||||
<Pencil className="h-4 w-4 mr-2" />
|
||||
Edit
|
||||
{t('admin.companies.details.edit')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -581,8 +746,8 @@ export default function AdminCompaniesPage() {
|
|||
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
|
||||
<DialogContent className="max-w-md max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit company</DialogTitle>
|
||||
<DialogDescription>Update company information</DialogDescription>
|
||||
<DialogTitle>{t('admin.companies.edit.title')}</DialogTitle>
|
||||
<DialogDescription>{t('admin.companies.edit.subtitle')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<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 })}
|
||||
id="edit-active"
|
||||
/>
|
||||
<Label htmlFor="edit-active">Active</Label>
|
||||
<Label htmlFor="edit-active">{t('admin.companies.fields.active')}</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
|
|
@ -600,11 +765,11 @@ export default function AdminCompaniesPage() {
|
|||
onCheckedChange={(checked) => setEditFormData({ ...editFormData, verified: checked })}
|
||||
id="edit-verified"
|
||||
/>
|
||||
<Label htmlFor="edit-verified">Verified</Label>
|
||||
<Label htmlFor="edit-verified">{t('admin.companies.stats.verified')}</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-name">Company name</Label>
|
||||
<Label htmlFor="edit-name">{t('admin.companies.create.name')}</Label>
|
||||
<Input
|
||||
id="edit-name"
|
||||
value={editFormData.name}
|
||||
|
|
@ -612,7 +777,7 @@ export default function AdminCompaniesPage() {
|
|||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-slug">Slug</Label>
|
||||
<Label htmlFor="edit-slug">{t('admin.companies.create.slug')}</Label>
|
||||
<Input
|
||||
id="edit-slug"
|
||||
value={editFormData.slug}
|
||||
|
|
@ -620,7 +785,7 @@ export default function AdminCompaniesPage() {
|
|||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-email">Email</Label>
|
||||
<Label htmlFor="edit-email">{t('admin.companies.create.email')}</Label>
|
||||
<Input
|
||||
id="edit-email"
|
||||
value={editFormData.email}
|
||||
|
|
@ -628,7 +793,7 @@ export default function AdminCompaniesPage() {
|
|||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-phone">Phone</Label>
|
||||
<Label htmlFor="edit-phone">{t('admin.companies.fields.phone')}</Label>
|
||||
<Input
|
||||
id="edit-phone"
|
||||
value={editFormData.phone}
|
||||
|
|
@ -636,7 +801,7 @@ export default function AdminCompaniesPage() {
|
|||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-website">Website</Label>
|
||||
<Label htmlFor="edit-website">{t('admin.companies.fields.website')}</Label>
|
||||
<Input
|
||||
id="edit-website"
|
||||
value={editFormData.website}
|
||||
|
|
@ -644,7 +809,7 @@ export default function AdminCompaniesPage() {
|
|||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-document">Document</Label>
|
||||
<Label htmlFor="edit-document">{t('admin.companies.fields.document')}</Label>
|
||||
<Input
|
||||
id="edit-document"
|
||||
value={editFormData.document}
|
||||
|
|
@ -652,7 +817,7 @@ export default function AdminCompaniesPage() {
|
|||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-address">Address</Label>
|
||||
<Label htmlFor="edit-address">{t('admin.companies.fields.address')}</Label>
|
||||
<Input
|
||||
id="edit-address"
|
||||
value={editFormData.address}
|
||||
|
|
@ -660,7 +825,7 @@ export default function AdminCompaniesPage() {
|
|||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-description">Description</Label>
|
||||
<Label htmlFor="edit-description">{t('admin.companies.fields.description')}</Label>
|
||||
<Input
|
||||
id="edit-description"
|
||||
value={editFormData.description}
|
||||
|
|
@ -669,10 +834,10 @@ export default function AdminCompaniesPage() {
|
|||
</div>
|
||||
</div>
|
||||
<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}>
|
||||
{updating && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||||
Save changes
|
||||
{t('admin.companies.edit.save')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
|
|
|||
|
|
@ -11,10 +11,12 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
|||
import { Separator } from "@/components/ui/separator"
|
||||
import { ArrowLeft, Loader2, Building2, DollarSign, FileText, Briefcase, MapPin, Clock } from "lucide-react"
|
||||
import { jobsApi, adminCompaniesApi, type CreateJobPayload, type AdminCompany } from "@/lib/api"
|
||||
import { useTranslation } from "@/lib/i18n"
|
||||
import { toast } from "sonner"
|
||||
|
||||
export default function NewJobPage() {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [companies, setCompanies] = useState<AdminCompany[]>([])
|
||||
const [loadingCompanies, setLoadingCompanies] = useState(true)
|
||||
|
|
@ -104,8 +106,8 @@ export default function NewJobPage() {
|
|||
<ArrowLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Post a job</h1>
|
||||
<p className="text-muted-foreground">Fill in the details below to create your job listing</p>
|
||||
<h1 className="text-3xl font-bold">{t('admin.jobs.newJob')}</h1>
|
||||
<p className="text-muted-foreground">{t('admin.jobs.edit.subtitle')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -114,13 +116,13 @@ export default function NewJobPage() {
|
|||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileText className="h-5 w-5" />
|
||||
Job Details
|
||||
{t('admin.jobs.details.title')}
|
||||
</CardTitle>
|
||||
<CardDescription>Basic information about this position</CardDescription>
|
||||
<CardDescription>{t('admin.jobs.details.description')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">Job Title *</Label>
|
||||
<Label htmlFor="title">{t('admin.jobs.edit.jobTitle')} *</Label>
|
||||
<Input
|
||||
id="title"
|
||||
placeholder="e.g. Senior Software Engineer"
|
||||
|
|
@ -132,7 +134,7 @@ export default function NewJobPage() {
|
|||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Job Description *</Label>
|
||||
<Label htmlFor="description">{t('admin.jobs.details.description')} *</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
placeholder="Describe the role, responsibilities, and requirements..."
|
||||
|
|
@ -147,7 +149,7 @@ export default function NewJobPage() {
|
|||
<div className="space-y-2">
|
||||
<Label htmlFor="location" className="flex items-center gap-1">
|
||||
<MapPin className="h-4 w-4" />
|
||||
Location
|
||||
{t('admin.candidates_page.table.location')}
|
||||
</Label>
|
||||
<Input
|
||||
id="location"
|
||||
|
|
@ -257,7 +259,7 @@ export default function NewJobPage() {
|
|||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<Label>Company *</Label>
|
||||
<Label>{t('admin.jobs.table.company')} *</Label>
|
||||
{loadingCompanies ? (
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
|
|
@ -288,7 +290,7 @@ export default function NewJobPage() {
|
|||
{/* Action Buttons */}
|
||||
<div className="flex justify-between items-center">
|
||||
<Button variant="outline" onClick={() => router.back()}>
|
||||
Cancel
|
||||
{t('admin.jobs.edit.cancel')}
|
||||
</Button>
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
|
|
@ -317,7 +319,7 @@ export default function NewJobPage() {
|
|||
) : (
|
||||
<>
|
||||
<Briefcase className="h-4 w-4 mr-2" />
|
||||
Publish Job
|
||||
{t('admin.jobs.edit.save')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -14,13 +14,11 @@ import {
|
|||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
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 { adminJobsApi, adminCompaniesApi, jobsApi, type AdminJob, type AdminCompany } from "@/lib/api"
|
||||
import { useTranslation } from "@/lib/i18n"
|
||||
|
||||
type AdminJobRow = {
|
||||
id: string
|
||||
|
|
@ -38,16 +36,15 @@ type AdminJobRow = {
|
|||
}
|
||||
|
||||
export default function AdminJobsPage() {
|
||||
const { t } = useTranslation()
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [jobs, setJobs] = useState<AdminJob[]>([])
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
|
||||
const [isViewDialogOpen, setIsViewDialogOpen] = useState(false)
|
||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
|
||||
const [selectedJob, setSelectedJob] = useState<AdminJobRow | null>(null)
|
||||
const [editForm, setEditForm] = useState<{ title?: string }>({})
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
||||
const [companies, setCompanies] = useState<AdminCompany[]>([])
|
||||
|
||||
// Pagination State
|
||||
const [page, setPage] = useState(1)
|
||||
|
|
@ -55,16 +52,6 @@ export default function AdminJobsPage() {
|
|||
const [totalPages, setTotalPages] = useState(1)
|
||||
const [totalJobs, setTotalJobs] = useState(0)
|
||||
|
||||
const [createForm, setCreateForm] = useState({
|
||||
title: "",
|
||||
company: "",
|
||||
location: "",
|
||||
type: "",
|
||||
level: "",
|
||||
salary: "",
|
||||
description: "",
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const loadJobs = async () => {
|
||||
try {
|
||||
|
|
@ -73,13 +60,6 @@ export default function AdminJobsPage() {
|
|||
// Fetch with pagination
|
||||
const jobsData = await adminJobsApi.list({ limit, page })
|
||||
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) {
|
||||
setTotalPages(Math.ceil((jobsData.pagination.total || 0) / limit))
|
||||
setTotalJobs(jobsData.pagination.total || 0)
|
||||
|
|
@ -90,7 +70,7 @@ export default function AdminJobsPage() {
|
|||
|
||||
} catch (error) {
|
||||
console.error("Failed to load jobs:", error)
|
||||
setErrorMessage("Unable to load jobs right now.")
|
||||
setErrorMessage(t('admin.jobs.table.error'))
|
||||
setJobs([])
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
|
|
@ -98,18 +78,7 @@ export default function AdminJobsPage() {
|
|||
}
|
||||
|
||||
loadJobs()
|
||||
|
||||
// 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
|
||||
}, [page, limit, t])
|
||||
|
||||
const jobRows = useMemo<AdminJobRow[]>(
|
||||
() =>
|
||||
|
|
@ -175,16 +144,14 @@ export default function AdminJobsPage() {
|
|||
|
||||
const handleDeleteJob = async (id: string) => {
|
||||
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 {
|
||||
console.log("[JOBS_PAGE] Calling jobsApi.delete...")
|
||||
await jobsApi.delete(id)
|
||||
console.log("[JOBS_PAGE] Job deleted successfully, updating local state")
|
||||
setJobs((prevJobs) => prevJobs.filter((job) => job.id !== id))
|
||||
} catch (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 () => {
|
||||
if (!selectedJob) return
|
||||
|
||||
console.log("[JOBS_PAGE] handleSaveEdit called for job:", selectedJob.id)
|
||||
console.log("[JOBS_PAGE] Edit form data:", editForm)
|
||||
|
||||
try {
|
||||
setIsLoading(true)
|
||||
console.log("[JOBS_PAGE] Calling jobsApi.update...")
|
||||
const updated = await jobsApi.update(selectedJob.id, editForm)
|
||||
console.log("[JOBS_PAGE] Job updated successfully:", updated)
|
||||
await jobsApi.update(selectedJob.id, editForm)
|
||||
// Reload jobs to get fresh data
|
||||
const jobsData = await adminJobsApi.list({ limit: 10, page: 1 })
|
||||
setJobs(jobsData.data ?? [])
|
||||
setIsEditDialogOpen(false)
|
||||
} catch (error) {
|
||||
console.error("[JOBS_PAGE] Failed to update job:", error)
|
||||
alert("Failed to update job")
|
||||
alert(t('admin.jobs.updateError'))
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
|
@ -218,7 +180,6 @@ export default function AdminJobsPage() {
|
|||
}
|
||||
|
||||
const handleNextPage = () => {
|
||||
// If we rely on generic "if array < limit then end" logic or strict meta total pages
|
||||
if (page < totalPages) setPage(page + 1)
|
||||
}
|
||||
|
||||
|
|
@ -227,13 +188,13 @@ export default function AdminJobsPage() {
|
|||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-foreground">Job management</h1>
|
||||
<p className="text-muted-foreground mt-1">Manage all jobs posted on the platform</p>
|
||||
<h1 className="text-3xl font-bold text-foreground">{t('admin.jobs.title')}</h1>
|
||||
<p className="text-muted-foreground mt-1">{t('admin.jobs.subtitle')}</p>
|
||||
</div>
|
||||
<Link href="/dashboard/jobs/new">
|
||||
<Button className="gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
New job
|
||||
{t('admin.jobs.newJob')}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
|
@ -242,33 +203,30 @@ export default function AdminJobsPage() {
|
|||
<Dialog open={isViewDialogOpen} onOpenChange={setIsViewDialogOpen}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Job Details</DialogTitle>
|
||||
<DialogTitle>{t('admin.jobs.details.title')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
{selectedJob && (
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
<Label className="text-muted-foreground">Status</Label>
|
||||
<Label className="text-muted-foreground">{t('admin.jobs.table.status')}</Label>
|
||||
<p><Badge>{selectedJob.status}</Badge></p>
|
||||
</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>
|
||||
</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">
|
||||
{selectedJob.description}
|
||||
</div>
|
||||
|
|
@ -276,7 +234,7 @@ export default function AdminJobsPage() {
|
|||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button onClick={() => setIsViewDialogOpen(false)}>Close</Button>
|
||||
<Button onClick={() => setIsViewDialogOpen(false)}>{t('admin.jobs.details.close')}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
|
@ -285,12 +243,12 @@ export default function AdminJobsPage() {
|
|||
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Job</DialogTitle>
|
||||
<DialogDescription>Update job details</DialogDescription>
|
||||
<DialogTitle>{t('admin.jobs.edit.title')}</DialogTitle>
|
||||
<DialogDescription>{t('admin.jobs.edit.subtitle')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-title">Job title</Label>
|
||||
<Label htmlFor="edit-title">{t('admin.jobs.edit.jobTitle')}</Label>
|
||||
<Input
|
||||
id="edit-title"
|
||||
value={editForm.title || ""}
|
||||
|
|
@ -299,8 +257,8 @@ export default function AdminJobsPage() {
|
|||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsEditDialogOpen(false)}>Cancel</Button>
|
||||
<Button onClick={handleSaveEdit}>Save Changes</Button>
|
||||
<Button variant="outline" onClick={() => setIsEditDialogOpen(false)}>{t('admin.jobs.edit.cancel')}</Button>
|
||||
<Button onClick={handleSaveEdit}>{t('admin.jobs.edit.save')}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
|
@ -309,25 +267,25 @@ export default function AdminJobsPage() {
|
|||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardDescription>Total jobs</CardDescription>
|
||||
<CardDescription>{t('admin.jobs.stats.total')}</CardDescription>
|
||||
<CardTitle className="text-3xl">{jobs.length}</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardDescription>Active jobs</CardDescription>
|
||||
<CardDescription>{t('admin.jobs.stats.active')}</CardDescription>
|
||||
<CardTitle className="text-3xl">{activeJobs}</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardDescription>Applications</CardDescription>
|
||||
<CardDescription>{t('admin.jobs.stats.applications')}</CardDescription>
|
||||
<CardTitle className="text-3xl">{totalApplications}</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardDescription>Conversion rate</CardDescription>
|
||||
<CardDescription>{t('admin.jobs.stats.conversion')}</CardDescription>
|
||||
<CardTitle className="text-3xl">
|
||||
{jobs.length > 0 ? Math.round((activeJobs / jobs.length) * 100) : 0}%
|
||||
</CardTitle>
|
||||
|
|
@ -342,7 +300,7 @@ export default function AdminJobsPage() {
|
|||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search jobs by title or company..."
|
||||
placeholder={t('admin.jobs.searchPlaceholder')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
|
|
@ -354,19 +312,18 @@ export default function AdminJobsPage() {
|
|||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Role</TableHead>
|
||||
<TableHead>Company</TableHead>
|
||||
{/* Removed Location and Type Headers */}
|
||||
<TableHead>Applications</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
<TableHead>{t('admin.jobs.table.role')}</TableHead>
|
||||
<TableHead>{t('admin.jobs.table.company')}</TableHead>
|
||||
<TableHead>{t('admin.jobs.table.applications')}</TableHead>
|
||||
<TableHead>{t('admin.jobs.table.status')}</TableHead>
|
||||
<TableHead className="text-right">{t('admin.jobs.table.actions')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center text-muted-foreground">
|
||||
Loading jobs...
|
||||
{t('admin.jobs.table.loading')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : errorMessage ? (
|
||||
|
|
@ -378,7 +335,7 @@ export default function AdminJobsPage() {
|
|||
) : filteredJobs.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center text-muted-foreground">
|
||||
No jobs found.
|
||||
{t('admin.jobs.table.empty')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
|
|
@ -386,7 +343,6 @@ export default function AdminJobsPage() {
|
|||
<TableRow key={job.id}>
|
||||
<TableCell className="font-medium">{job.title}</TableCell>
|
||||
<TableCell>{job.company}</TableCell>
|
||||
{/* Removed Location and Type Cells */}
|
||||
<TableCell>{job.applicationsCount ?? 0}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="default">{job.status ?? "Active"}</Badge>
|
||||
|
|
|
|||
|
|
@ -227,11 +227,11 @@ export default function AdminUsersPage() {
|
|||
|
||||
const getRoleBadge = (role: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
superadmin: "Super Admin",
|
||||
admin: "Company Admin",
|
||||
recruiter: "Recruiter",
|
||||
candidate: "Candidate",
|
||||
company: "Company"
|
||||
superadmin: t('admin.users.roles.superadmin'),
|
||||
admin: t('admin.users.roles.admin'),
|
||||
recruiter: t('admin.users.roles.recruiter'),
|
||||
candidate: t('admin.users.roles.candidate'),
|
||||
company: t('admin.users.roles.admin') // Fallback for 'company' role legacy
|
||||
}
|
||||
const colors: Record<string, "default" | "secondary" | "destructive" | "outline"> = {
|
||||
superadmin: "destructive",
|
||||
|
|
@ -295,15 +295,15 @@ export default function AdminUsersPage() {
|
|||
type="password"
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
placeholder="Secure password"
|
||||
placeholder={t('admin.users.form.password')}
|
||||
/>
|
||||
</div>
|
||||
{currentUser?.role === 'superadmin' && (
|
||||
<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 })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a company" />
|
||||
<SelectValue placeholder={t('admin.users.form.select_company')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{companies.map((company) => (
|
||||
|
|
@ -319,11 +319,11 @@ export default function AdminUsersPage() {
|
|||
<Label htmlFor="status">{t('admin.users.table.status')}</Label>
|
||||
<Select value={formData.status} onValueChange={(v) => setFormData({ ...formData, status: v })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
<SelectValue placeholder={t('admin.users.form.status_placeholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="active">Active</SelectItem>
|
||||
<SelectItem value="inactive">Inactive</SelectItem>
|
||||
<SelectItem value="active">{t('admin.users.statuses.active')}</SelectItem>
|
||||
<SelectItem value="inactive">{t('admin.users.statuses.inactive')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
|
@ -334,10 +334,10 @@ export default function AdminUsersPage() {
|
|||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="superadmin">Super Admin</SelectItem>
|
||||
<SelectItem value="admin">Company admin</SelectItem>
|
||||
<SelectItem value="recruiter">Recruiter</SelectItem>
|
||||
<SelectItem value="candidate">Candidate</SelectItem>
|
||||
<SelectItem value="superadmin">{t('admin.users.roles.superadmin')}</SelectItem>
|
||||
<SelectItem value="admin">{t('admin.users.roles.admin')}</SelectItem>
|
||||
<SelectItem value="recruiter">{t('admin.users.roles.recruiter')}</SelectItem>
|
||||
<SelectItem value="candidate">{t('admin.users.roles.candidate')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
|
@ -385,7 +385,7 @@ export default function AdminUsersPage() {
|
|||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-role">Role</Label>
|
||||
<Label htmlFor="edit-role">{t('admin.users.table.role')}</Label>
|
||||
<Select
|
||||
value={editFormData.role}
|
||||
onValueChange={(v) => setEditFormData({ ...editFormData, role: v })}
|
||||
|
|
@ -395,15 +395,15 @@ export default function AdminUsersPage() {
|
|||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="superadmin">Super Admin</SelectItem>
|
||||
<SelectItem value="admin">Company admin</SelectItem>
|
||||
<SelectItem value="recruiter">Recruiter</SelectItem>
|
||||
<SelectItem value="candidate">Candidate</SelectItem>
|
||||
<SelectItem value="superadmin">{t('admin.users.roles.superadmin')}</SelectItem>
|
||||
<SelectItem value="admin">{t('admin.users.roles.admin')}</SelectItem>
|
||||
<SelectItem value="recruiter">{t('admin.users.roles.recruiter')}</SelectItem>
|
||||
<SelectItem value="candidate">{t('admin.users.roles.candidate')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-status">Status</Label>
|
||||
<Label htmlFor="edit-status">{t('admin.users.table.status')}</Label>
|
||||
<Select
|
||||
value={editFormData.status}
|
||||
onValueChange={(v) => setEditFormData({ ...editFormData, status: v })}
|
||||
|
|
@ -413,8 +413,8 @@ export default function AdminUsersPage() {
|
|||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="active">Active</SelectItem>
|
||||
<SelectItem value="inactive">Inactive</SelectItem>
|
||||
<SelectItem value="active">{t('admin.users.statuses.active')}</SelectItem>
|
||||
<SelectItem value="inactive">{t('admin.users.statuses.inactive')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -907,6 +907,23 @@
|
|||
"delete_success": "User deleted!",
|
||||
"delete_error": "Failed to delete user",
|
||||
"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": {
|
||||
|
|
@ -949,6 +966,114 @@
|
|||
"hired": "Hired",
|
||||
"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": {
|
||||
|
|
|
|||
|
|
@ -967,6 +967,23 @@
|
|||
"delete_success": "Usuário excluído!",
|
||||
"delete_error": "Falha ao excluir usuário",
|
||||
"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": {
|
||||
|
|
@ -1009,6 +1026,114 @@
|
|||
"hired": "Contratado",
|
||||
"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": {
|
||||
|
|
|
|||
Loading…
Reference in a new issue