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:
parent
23ff95f1a8
commit
ad558bc656
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)
|
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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,19 @@ import "time"
|
||||||
|
|
||||||
// Company represents a Tenant in the system.
|
// Company represents a Tenant in the system.
|
||||||
type Company struct {
|
type Company struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Document *string `json:"document,omitempty"` // CNPJ, EIN, VAT
|
Slug string `json:"slug"`
|
||||||
Contact *string `json:"contact,omitempty"`
|
Document *string `json:"document,omitempty"` // CNPJ, EIN, VAT
|
||||||
Status string `json:"status"` // "ACTIVE", "INACTIVE"
|
Contact *string `json:"contact,omitempty"` // Email
|
||||||
CreatedAt time.Time `json:"created_at"`
|
Phone *string `json:"phone,omitempty"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
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.
|
// NewCompany creates a new Company instance with defaults.
|
||||||
|
|
@ -18,6 +24,8 @@ 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",
|
||||||
Document: document,
|
Document: document,
|
||||||
Contact: contact,
|
Contact: contact,
|
||||||
Status: "ACTIVE",
|
Status: "ACTIVE",
|
||||||
|
|
|
||||||
|
|
@ -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"`
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ func (uc *RegisterCandidateUseCase) Execute(ctx context.Context, input dto.Regis
|
||||||
nil, // No document for candidates
|
nil, // No document for candidates
|
||||||
nil, // No contact - will use user's contact info
|
nil, // No contact - will use user's contact info
|
||||||
)
|
)
|
||||||
|
candidateCompany.Type = "CANDIDATE_WORKSPACE"
|
||||||
|
|
||||||
savedCompany, err := uc.companyRepo.Save(ctx, candidateCompany)
|
savedCompany, err := uc.companyRepo.Save(ctx, candidateCompany)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,21 +20,30 @@ 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
|
||||||
`
|
`
|
||||||
|
|
||||||
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,
|
||||||
"company",
|
company.Type,
|
||||||
company.Document,
|
company.Document,
|
||||||
company.Contact, // mapped to email
|
company.Contact, // email
|
||||||
"{}", // description as JSON
|
company.Phone,
|
||||||
true, // verified
|
company.Website,
|
||||||
|
company.Address,
|
||||||
|
company.Description,
|
||||||
|
true, // verified
|
||||||
company.Status == "ACTIVE",
|
company.Status == "ACTIVE",
|
||||||
company.CreatedAt,
|
company.CreatedAt,
|
||||||
company.UpdatedAt,
|
company.UpdatedAt,
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,11 @@ 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`
|
// Count Total
|
||||||
|
countQuery := `SELECT COUNT(*) FROM companies WHERE type != 'CANDIDATE_WORKSPACE'`
|
||||||
var countArgs []interface{}
|
var countArgs []interface{}
|
||||||
if verified != nil {
|
if verified != nil {
|
||||||
countQuery += " WHERE verified = $1"
|
countQuery += " AND verified = $1"
|
||||||
countArgs = append(countArgs, *verified)
|
countArgs = append(countArgs, *verified)
|
||||||
}
|
}
|
||||||
var total int
|
var total int
|
||||||
|
|
@ -40,11 +41,12 @@ 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 != 'CANDIDATE_WORKSPACE'
|
||||||
`
|
`
|
||||||
|
|
||||||
var args []interface{}
|
var args []interface{}
|
||||||
if verified != nil {
|
if verified != nil {
|
||||||
baseQuery += " WHERE verified = $1"
|
baseQuery += " AND verified = $1"
|
||||||
args = append(args, *verified)
|
args = append(args, *verified)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -610,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
|
||||||
|
|
@ -728,9 +764,28 @@ 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
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 {
|
} 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">
|
||||||
<Button variant="ghost" size="icon" onClick={() => handleView(company)}>
|
<div className="flex items-center justify-end gap-2">
|
||||||
<Eye className="h-4 w-4" />
|
<Button variant="ghost" size="icon" onClick={() => handleView(company)}>
|
||||||
</Button>
|
<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>
|
</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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue