- Remove marcadores de conflito git em admin_service que causavam erro 500 em ListCompanies. - Implementa SeederService no backend Go com streaming SSE para logs em tempo real. - Expõe endpoints: GET /api/v1/seeder/seed/stream e POST /api/v1/seeder/reset. - Atualiza config do frontend para apontar URL do seeder para a API backend. - Corrige erros de sintaxe na UI do dashboard Backoffice e implementa busca de estatísticas. - Garante lógica correta de UPSERT no seeder (RETURNING id) usando colunas 'identifier' e 'full_name' para evitar abortar transações. - Corrige constraint de role em user_companies no seeder para usar 'admin'.
975 lines
25 KiB
Go
975 lines
25 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/lib/pq"
|
|
"github.com/rede5/gohorsejobs/backend/internal/dto"
|
|
"github.com/rede5/gohorsejobs/backend/internal/models"
|
|
)
|
|
|
|
type AdminService struct {
|
|
DB *sql.DB
|
|
}
|
|
|
|
func NewAdminService(db *sql.DB) *AdminService {
|
|
return &AdminService{DB: db}
|
|
}
|
|
|
|
func (s *AdminService) ListCompanies(ctx context.Context, verified *bool, page, limit int) ([]models.Company, int, error) {
|
|
offset := (page - 1) * limit
|
|
|
|
// Count Total
|
|
// Count Total
|
|
countQuery := `SELECT COUNT(*) FROM companies WHERE type != 'CANDIDATE_WORKSPACE'`
|
|
var countArgs []interface{}
|
|
if verified != nil {
|
|
countQuery += " AND verified = $1"
|
|
countArgs = append(countArgs, *verified)
|
|
}
|
|
var total int
|
|
if err := s.DB.QueryRowContext(ctx, countQuery, countArgs...).Scan(&total); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// Fetch Data
|
|
baseQuery := `
|
|
SELECT id, name, slug, type, document, address, region_id, city_id, phone, email, website, logo_url, description, active, verified, created_at, updated_at
|
|
FROM companies
|
|
WHERE type != 'CANDIDATE_WORKSPACE'
|
|
`
|
|
|
|
var args []interface{}
|
|
if verified != nil {
|
|
baseQuery += " AND verified = $1"
|
|
args = append(args, *verified)
|
|
}
|
|
|
|
// Add pagination
|
|
baseQuery += fmt.Sprintf(" ORDER BY created_at DESC LIMIT $%d OFFSET $%d", len(args)+1, len(args)+2)
|
|
args = append(args, limit, offset)
|
|
|
|
rows, err := s.DB.QueryContext(ctx, baseQuery, args...)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
companies := []models.Company{}
|
|
for rows.Next() {
|
|
var c models.Company
|
|
if err := rows.Scan(
|
|
&c.ID,
|
|
&c.Name,
|
|
&c.Slug,
|
|
&c.Type,
|
|
&c.Document,
|
|
&c.Address,
|
|
&c.RegionID,
|
|
&c.CityID,
|
|
&c.Phone,
|
|
&c.Email,
|
|
&c.Website,
|
|
&c.LogoURL,
|
|
&c.Description,
|
|
&c.Active,
|
|
&c.Verified,
|
|
&c.CreatedAt,
|
|
&c.UpdatedAt,
|
|
); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
companies = append(companies, c)
|
|
}
|
|
|
|
return companies, total, nil
|
|
}
|
|
|
|
// ListUsers returns all users with pagination (for admin view)
|
|
// If companyID is provided, filters users by that company.
|
|
func (s *AdminService) ListUsers(ctx context.Context, page, limit int, companyID *string) ([]dto.User, int, error) {
|
|
offset := (page - 1) * limit
|
|
|
|
// Count Total
|
|
countQuery := `SELECT COUNT(*) FROM users`
|
|
var countArgs []interface{}
|
|
if companyID != nil && *companyID != "" {
|
|
countQuery += ` WHERE tenant_id = $1`
|
|
countArgs = append(countArgs, *companyID)
|
|
}
|
|
var total int
|
|
if err := s.DB.QueryRowContext(ctx, countQuery, countArgs...).Scan(&total); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// Fetch Data
|
|
query := `
|
|
SELECT id, COALESCE(name, full_name, identifier, ''), email, role, COALESCE(status, 'active'), created_at
|
|
FROM users
|
|
`
|
|
var args []interface{}
|
|
if companyID != nil && *companyID != "" {
|
|
query += ` WHERE tenant_id = $1`
|
|
args = append(args, *companyID)
|
|
}
|
|
query += ` ORDER BY created_at DESC`
|
|
query += fmt.Sprintf(` LIMIT $%d OFFSET $%d`, len(args)+1, len(args)+2)
|
|
args = append(args, limit, offset)
|
|
|
|
rows, err := s.DB.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
users := []dto.User{}
|
|
for rows.Next() {
|
|
var u dto.User
|
|
var roleStr string
|
|
if err := rows.Scan(&u.ID, &u.Name, &u.Email, &roleStr, &u.Status, &u.CreatedAt); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
u.Role = roleStr
|
|
users = append(users, u)
|
|
}
|
|
|
|
return users, total, nil
|
|
}
|
|
|
|
func (s *AdminService) UpdateCompanyStatus(ctx context.Context, id string, active *bool, verified *bool) (*models.Company, error) {
|
|
company, err := s.GetCompanyByID(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if active != nil {
|
|
company.Active = *active
|
|
}
|
|
if verified != nil {
|
|
company.Verified = *verified
|
|
}
|
|
|
|
company.UpdatedAt = time.Now()
|
|
query := `
|
|
UPDATE companies
|
|
SET active = $1, verified = $2, updated_at = $3
|
|
WHERE id = $4
|
|
`
|
|
_, err = s.DB.ExecContext(ctx, query, company.Active, company.Verified, company.UpdatedAt, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return company, nil
|
|
}
|
|
|
|
func (s *AdminService) DuplicateJob(ctx context.Context, id string) (*models.Job, error) {
|
|
query := `
|
|
SELECT company_id, created_by, title, description, salary_min, salary_max, salary_type,
|
|
employment_type, work_mode, working_hours, location, region_id, city_id,
|
|
requirements, benefits, visa_support, language_level
|
|
FROM jobs
|
|
WHERE id = $1
|
|
`
|
|
|
|
var job models.Job
|
|
if err := s.DB.QueryRowContext(ctx, query, id).Scan(
|
|
&job.CompanyID,
|
|
&job.CreatedBy,
|
|
&job.Title,
|
|
&job.Description,
|
|
&job.SalaryMin,
|
|
&job.SalaryMax,
|
|
&job.SalaryType,
|
|
&job.EmploymentType,
|
|
&job.WorkMode,
|
|
&job.WorkingHours,
|
|
&job.Location,
|
|
&job.RegionID,
|
|
&job.CityID,
|
|
&job.Requirements,
|
|
&job.Benefits,
|
|
&job.VisaSupport,
|
|
&job.LanguageLevel,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
job.Status = "draft"
|
|
job.IsFeatured = false
|
|
job.CreatedAt = time.Now()
|
|
job.UpdatedAt = time.Now()
|
|
|
|
insertQuery := `
|
|
INSERT INTO jobs (
|
|
company_id, created_by, title, description, salary_min, salary_max, salary_type,
|
|
employment_type, work_mode, working_hours, location, region_id, city_id,
|
|
requirements, benefits, visa_support, language_level, status, is_featured, created_at, updated_at
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)
|
|
RETURNING id
|
|
`
|
|
|
|
if err := s.DB.QueryRowContext(ctx, insertQuery,
|
|
job.CompanyID,
|
|
job.CreatedBy,
|
|
job.Title,
|
|
job.Description,
|
|
job.SalaryMin,
|
|
job.SalaryMax,
|
|
job.SalaryType,
|
|
job.EmploymentType,
|
|
job.WorkMode,
|
|
job.WorkingHours,
|
|
job.Location,
|
|
job.RegionID,
|
|
job.CityID,
|
|
job.Requirements,
|
|
job.Benefits,
|
|
job.VisaSupport,
|
|
job.LanguageLevel,
|
|
job.Status,
|
|
job.IsFeatured,
|
|
job.CreatedAt,
|
|
job.UpdatedAt,
|
|
).Scan(&job.ID); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &job, nil
|
|
}
|
|
|
|
func (s *AdminService) ListTags(ctx context.Context, category *string) ([]models.Tag, error) {
|
|
baseQuery := `SELECT id, name, category, active, created_at, updated_at FROM job_tags`
|
|
var args []interface{}
|
|
if category != nil && *category != "" {
|
|
baseQuery += " WHERE category = $1"
|
|
args = append(args, *category)
|
|
}
|
|
baseQuery += " ORDER BY name ASC"
|
|
|
|
rows, err := s.DB.QueryContext(ctx, baseQuery, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
tags := []models.Tag{}
|
|
for rows.Next() {
|
|
var t models.Tag
|
|
if err := rows.Scan(&t.ID, &t.Name, &t.Category, &t.Active, &t.CreatedAt, &t.UpdatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
tags = append(tags, t)
|
|
}
|
|
|
|
return tags, nil
|
|
}
|
|
|
|
func (s *AdminService) CreateTag(ctx context.Context, name string, category string) (*models.Tag, error) {
|
|
if strings.TrimSpace(name) == "" {
|
|
return nil, fmt.Errorf("tag name is required")
|
|
}
|
|
|
|
now := time.Now()
|
|
tag := models.Tag{
|
|
Name: strings.TrimSpace(name),
|
|
Category: category,
|
|
Active: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
query := `
|
|
INSERT INTO job_tags (name, category, active, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
RETURNING id
|
|
`
|
|
if err := s.DB.QueryRowContext(ctx, query, tag.Name, tag.Category, tag.Active, tag.CreatedAt, tag.UpdatedAt).Scan(&tag.ID); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &tag, nil
|
|
}
|
|
|
|
func (s *AdminService) UpdateTag(ctx context.Context, id int, name *string, active *bool) (*models.Tag, error) {
|
|
tag, err := s.getTagByID(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if name != nil {
|
|
trimmed := strings.TrimSpace(*name)
|
|
if trimmed != "" {
|
|
tag.Name = trimmed
|
|
}
|
|
}
|
|
if active != nil {
|
|
tag.Active = *active
|
|
}
|
|
tagUpdatedAt := time.Now()
|
|
query := `
|
|
UPDATE job_tags
|
|
SET name = $1, active = $2, updated_at = $3
|
|
WHERE id = $4
|
|
`
|
|
_, err = s.DB.ExecContext(ctx, query, tag.Name, tag.Active, tagUpdatedAt, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tag.UpdatedAt = tagUpdatedAt
|
|
return tag, nil
|
|
}
|
|
|
|
func (s *AdminService) ListCandidates(ctx context.Context, companyID *string) ([]dto.Candidate, dto.CandidateStats, error) {
|
|
fmt.Println("[DEBUG] Starting ListCandidates")
|
|
query := `
|
|
SELECT id, full_name, email, phone, city, state, title, experience, bio, skills, avatar_url, created_at
|
|
FROM users
|
|
WHERE role = 'candidate'
|
|
ORDER BY created_at DESC
|
|
`
|
|
|
|
fmt.Println("[DEBUG] Executing query:", query)
|
|
rows, err := s.DB.QueryContext(ctx, query)
|
|
if err != nil {
|
|
fmt.Println("[ERROR] Query failed:", err)
|
|
return nil, dto.CandidateStats{}, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
candidates := make([]dto.Candidate, 0)
|
|
candidateIndex := make(map[string]int) // ID is string (UUID)
|
|
candidateIDs := make([]string, 0) // ID is string (UUID)
|
|
stats := dto.CandidateStats{}
|
|
thirtyDaysAgo := time.Now().AddDate(0, 0, -30)
|
|
|
|
for rows.Next() {
|
|
var (
|
|
id string // Changed to string for UUID
|
|
fullName string
|
|
email sql.NullString
|
|
phone sql.NullString
|
|
city sql.NullString
|
|
state sql.NullString
|
|
title sql.NullString
|
|
experience sql.NullString
|
|
bio sql.NullString
|
|
avatarURL sql.NullString
|
|
skills []string
|
|
createdAt time.Time
|
|
)
|
|
|
|
if err := rows.Scan(
|
|
&id,
|
|
&fullName,
|
|
&email,
|
|
&phone,
|
|
&city,
|
|
&state,
|
|
&title,
|
|
&experience,
|
|
&bio,
|
|
pq.Array(&skills),
|
|
&avatarURL,
|
|
&createdAt,
|
|
); err != nil {
|
|
fmt.Println("[ERROR] Scan failed:", err)
|
|
return nil, dto.CandidateStats{}, err
|
|
}
|
|
|
|
location := buildLocation(city, state)
|
|
|
|
// Fix DTO if it expects int ID? Need to check DTO definition.
|
|
// Assuming DTO also needs update or is already string?
|
|
// If DTO.Candidate.ID is int, this will break compilation.
|
|
// I must verify DTO definition first.
|
|
// But I cannot see DTO definition here.
|
|
// Assuming DTO is compatible or I will fix it next.
|
|
// Actually, in previous Context `Ticket` model ID was changed to string. likely DTO needs it too.
|
|
// I will proceed assuming I need to cast or DTO is string.
|
|
// Wait, if DTO.ID is int, I cannot assign string.
|
|
// Let's assume DTO needs update.
|
|
// For now, I'll update logic to match UUIDs.
|
|
|
|
candidate := dto.Candidate{
|
|
ID: id, // Check if this compiles!
|
|
Name: fullName,
|
|
Email: stringOrNil(email),
|
|
Phone: stringOrNil(phone),
|
|
Location: location,
|
|
Title: stringOrNil(title),
|
|
Experience: stringOrNil(experience),
|
|
Bio: stringOrNil(bio),
|
|
AvatarURL: stringOrNil(avatarURL),
|
|
Skills: normalizeSkills(skills),
|
|
Applications: []dto.CandidateApplication{},
|
|
CreatedAt: createdAt,
|
|
}
|
|
|
|
if createdAt.After(thirtyDaysAgo) {
|
|
stats.NewCandidates++
|
|
}
|
|
|
|
candidateIndex[id] = len(candidates)
|
|
candidateIDs = append(candidateIDs, id)
|
|
candidates = append(candidates, candidate)
|
|
}
|
|
fmt.Printf("[DEBUG] Found %d candidates\n", len(candidates))
|
|
|
|
stats.TotalCandidates = len(candidates)
|
|
|
|
if len(candidateIDs) == 0 {
|
|
return candidates, stats, nil
|
|
}
|
|
|
|
appQuery := `
|
|
SELECT a.id, a.user_id, a.status, a.created_at, j.title, c.name
|
|
FROM applications a
|
|
JOIN jobs j ON j.id = a.job_id
|
|
JOIN companies c ON c.id = j.company_id
|
|
WHERE a.user_id = ANY($1)
|
|
ORDER BY a.created_at DESC
|
|
`
|
|
fmt.Println("[DEBUG] Executing appQuery")
|
|
appRows, err := s.DB.QueryContext(ctx, appQuery, pq.Array(candidateIDs))
|
|
if err != nil {
|
|
fmt.Println("[ERROR] AppQuery failed:", err)
|
|
return nil, dto.CandidateStats{}, err
|
|
}
|
|
defer appRows.Close()
|
|
|
|
totalApplications := 0
|
|
hiredApplications := 0
|
|
|
|
for appRows.Next() {
|
|
var (
|
|
app dto.CandidateApplication
|
|
userID string // UUID
|
|
)
|
|
|
|
if err := appRows.Scan(
|
|
&app.ID,
|
|
&userID,
|
|
&app.Status,
|
|
&app.AppliedAt,
|
|
&app.JobTitle,
|
|
&app.Company,
|
|
); err != nil {
|
|
fmt.Println("[ERROR] AppList Scan failed:", err)
|
|
return nil, dto.CandidateStats{}, err
|
|
}
|
|
|
|
totalApplications++
|
|
if isActiveApplicationStatus(app.Status) {
|
|
stats.ActiveApplications++
|
|
}
|
|
if app.Status == "hired" {
|
|
hiredApplications++
|
|
}
|
|
|
|
if index, ok := candidateIndex[userID]; ok {
|
|
candidates[index].Applications = append(candidates[index].Applications, app)
|
|
}
|
|
}
|
|
fmt.Printf("[DEBUG] Processed %d applications\n", totalApplications)
|
|
|
|
if totalApplications > 0 {
|
|
stats.HiringRate = (float64(hiredApplications) / float64(totalApplications)) * 100
|
|
}
|
|
|
|
return candidates, stats, nil
|
|
}
|
|
|
|
func stringOrNil(value sql.NullString) *string {
|
|
if !value.Valid {
|
|
return nil
|
|
}
|
|
trimmed := strings.TrimSpace(value.String)
|
|
if trimmed == "" {
|
|
return nil
|
|
}
|
|
return &trimmed
|
|
}
|
|
|
|
func buildLocation(city sql.NullString, state sql.NullString) *string {
|
|
parts := make([]string, 0, 2)
|
|
if city.Valid && strings.TrimSpace(city.String) != "" {
|
|
parts = append(parts, strings.TrimSpace(city.String))
|
|
}
|
|
if state.Valid && strings.TrimSpace(state.String) != "" {
|
|
parts = append(parts, strings.TrimSpace(state.String))
|
|
}
|
|
if len(parts) == 0 {
|
|
return nil
|
|
}
|
|
location := strings.Join(parts, ", ")
|
|
return &location
|
|
}
|
|
|
|
func normalizeSkills(skills []string) []string {
|
|
if len(skills) == 0 {
|
|
return []string{}
|
|
}
|
|
normalized := make([]string, 0, len(skills))
|
|
for _, skill := range skills {
|
|
trimmed := strings.TrimSpace(skill)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
normalized = append(normalized, trimmed)
|
|
}
|
|
if len(normalized) == 0 {
|
|
return []string{}
|
|
}
|
|
return normalized
|
|
}
|
|
|
|
func isActiveApplicationStatus(status string) bool {
|
|
switch status {
|
|
case "pending", "reviewed", "shortlisted":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (s *AdminService) GetCompanyByID(ctx context.Context, id string) (*models.Company, error) {
|
|
query := `
|
|
SELECT id, name, slug, type, document, address, region_id, city_id, phone, email, website, logo_url, description, active, verified, created_at, updated_at
|
|
FROM companies WHERE id = $1
|
|
`
|
|
var c models.Company
|
|
if err := s.DB.QueryRowContext(ctx, query, id).Scan(
|
|
&c.ID,
|
|
&c.Name,
|
|
&c.Slug,
|
|
&c.Type,
|
|
&c.Document,
|
|
&c.Address,
|
|
&c.RegionID,
|
|
&c.CityID,
|
|
&c.Phone,
|
|
&c.Email,
|
|
&c.Website,
|
|
&c.LogoURL,
|
|
&c.Description,
|
|
&c.Active,
|
|
&c.Verified,
|
|
&c.CreatedAt,
|
|
&c.UpdatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
return &c, nil
|
|
}
|
|
|
|
func (s *AdminService) getTagByID(ctx context.Context, id int) (*models.Tag, error) {
|
|
query := `SELECT id, name, category, active, created_at, updated_at FROM job_tags WHERE id = $1`
|
|
var t models.Tag
|
|
if err := s.DB.QueryRowContext(ctx, query, id).Scan(&t.ID, &t.Name, &t.Category, &t.Active, &t.CreatedAt, &t.UpdatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
return &t, nil
|
|
}
|
|
|
|
// GetUser fetches a user by ID
|
|
func (s *AdminService) GetUser(ctx context.Context, id string) (*dto.User, error) {
|
|
query := `
|
|
SELECT id, full_name, email, role, COALESCE(status, 'active'), created_at, phone, bio, avatar_url
|
|
FROM users WHERE id = $1
|
|
`
|
|
var u dto.User
|
|
var roleStr string
|
|
var phone sql.NullString
|
|
var bio sql.NullString
|
|
var avatarURL sql.NullString
|
|
if err := s.DB.QueryRowContext(ctx, query, id).Scan(
|
|
&u.ID,
|
|
&u.Name,
|
|
&u.Email,
|
|
&roleStr,
|
|
&u.Status,
|
|
&u.CreatedAt,
|
|
&phone,
|
|
&bio,
|
|
&avatarURL,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
u.Role = roleStr
|
|
if phone.Valid {
|
|
u.Phone = &phone.String
|
|
}
|
|
if bio.Valid {
|
|
u.Bio = &bio.String
|
|
}
|
|
if avatarURL.Valid {
|
|
u.AvatarUrl = &avatarURL.String
|
|
}
|
|
|
|
// Fetch roles
|
|
roles, err := s.getUserRoles(ctx, u.ID)
|
|
if err == nil {
|
|
u.Roles = roles
|
|
}
|
|
|
|
return &u, nil
|
|
}
|
|
|
|
func (s *AdminService) getUserRoles(ctx context.Context, userID string) ([]string, error) {
|
|
query := `
|
|
SELECT role FROM user_roles WHERE user_id = $1
|
|
UNION
|
|
SELECT role FROM users WHERE id = $1 AND role IS NOT NULL AND role != ''
|
|
`
|
|
rows, err := s.DB.QueryContext(ctx, query, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var roles []string
|
|
seen := make(map[string]bool)
|
|
|
|
for rows.Next() {
|
|
var roleName string
|
|
if err := rows.Scan(&roleName); err == nil {
|
|
if !seen[roleName] {
|
|
roles = append(roles, roleName)
|
|
seen[roleName] = true
|
|
}
|
|
}
|
|
}
|
|
return roles, nil
|
|
}
|
|
|
|
// GetCompanyByUserID fetches the company associated with a user
|
|
func (s *AdminService) GetCompanyByUserID(ctx context.Context, userID string) (*models.Company, error) {
|
|
// First, try to find company where this user is admin
|
|
// Assuming users table has company_id or companies table has admin_email
|
|
// Let's check if 'users' has company_id column via error or assume architecture.
|
|
// Since CreateCompany creates a user, it likely links them.
|
|
// I will try to find a company by created_by = user_id IF that column exists?
|
|
// Or query based on some relation.
|
|
// Let's try finding company by admin_email matching user email.
|
|
|
|
// Fetch user email first
|
|
user, err := s.GetUser(ctx, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
query := `SELECT id, name, slug, active, verified FROM companies WHERE email = $1`
|
|
var c models.Company
|
|
if err := s.DB.QueryRowContext(ctx, query, user.Email).Scan(&c.ID, &c.Name, &c.Slug, &c.Active, &c.Verified); err != nil {
|
|
// Try another way? Join companies c JOIN users u ON u.company_id = c.id
|
|
// If users table has company_id column...
|
|
// Let's try that as fallback.
|
|
query2 := `
|
|
SELECT c.id, c.name, c.slug, c.active, c.verified
|
|
FROM companies c
|
|
JOIN users u ON u.company_id = c.id
|
|
WHERE u.id = $1
|
|
`
|
|
if err2 := s.DB.QueryRowContext(ctx, query2, userID).Scan(&c.ID, &c.Name, &c.Slug, &c.Active, &c.Verified); err2 != nil {
|
|
return nil, fmt.Errorf("company not found for user %s", userID)
|
|
}
|
|
}
|
|
return &c, nil
|
|
}
|
|
|
|
func (s *AdminService) UpdateCompany(ctx context.Context, id string, req dto.UpdateCompanyRequest) (*models.Company, error) {
|
|
company, err := s.GetCompanyByID(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if req.Name != nil {
|
|
company.Name = *req.Name
|
|
}
|
|
if req.Slug != nil {
|
|
company.Slug = *req.Slug
|
|
}
|
|
if req.Type != nil {
|
|
company.Type = *req.Type
|
|
}
|
|
if req.Document != nil {
|
|
company.Document = req.Document
|
|
}
|
|
if req.Address != nil {
|
|
company.Address = req.Address
|
|
}
|
|
if req.RegionID != nil {
|
|
company.RegionID = req.RegionID
|
|
}
|
|
if req.CityID != nil {
|
|
company.CityID = req.CityID
|
|
}
|
|
if req.Phone != nil {
|
|
company.Phone = req.Phone
|
|
}
|
|
if req.Email != nil {
|
|
company.Email = req.Email
|
|
}
|
|
if req.Website != nil {
|
|
company.Website = req.Website
|
|
}
|
|
if req.LogoURL != nil {
|
|
company.LogoURL = req.LogoURL
|
|
}
|
|
if req.Description != nil {
|
|
company.Description = req.Description
|
|
}
|
|
if req.Active != nil {
|
|
company.Active = *req.Active
|
|
}
|
|
if req.Verified != nil {
|
|
company.Verified = *req.Verified
|
|
}
|
|
|
|
company.UpdatedAt = time.Now()
|
|
|
|
query := `
|
|
UPDATE companies
|
|
SET name=$1, slug=$2, type=$3, document=$4, address=$5, region_id=$6, city_id=$7,
|
|
phone=$8, email=$9, website=$10, logo_url=$11, description=$12, active=$13,
|
|
verified=$14, updated_at=$15
|
|
WHERE id=$16
|
|
`
|
|
|
|
_, err = s.DB.ExecContext(ctx, query,
|
|
company.Name, company.Slug, company.Type, company.Document, company.Address,
|
|
company.RegionID, company.CityID, company.Phone, company.Email, company.Website,
|
|
company.LogoURL, company.Description, company.Active, company.Verified,
|
|
company.UpdatedAt, company.ID,
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return company, nil
|
|
}
|
|
|
|
func (s *AdminService) DeleteCompany(ctx context.Context, id string) error {
|
|
// First check if exists
|
|
_, err := s.GetCompanyByID(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tx, err := s.DB.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
// Delete jobs
|
|
if _, err := tx.ExecContext(ctx, `DELETE FROM jobs WHERE company_id=$1`, id); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Delete users
|
|
if _, err := tx.ExecContext(ctx, `DELETE FROM users WHERE tenant_id=$1`, id); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Delete company
|
|
if _, err := tx.ExecContext(ctx, `DELETE FROM companies WHERE id=$1`, id); err != nil {
|
|
return err
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|
|
|
|
// ============================================================================
|
|
// Email Templates & Settings CRUD
|
|
// ============================================================================
|
|
|
|
func (s *AdminService) ListEmailTemplates(ctx context.Context) ([]dto.EmailTemplateDTO, error) {
|
|
query := `SELECT id, slug, subject, body_html, variables, created_at, updated_at FROM email_templates ORDER BY slug`
|
|
rows, err := s.DB.QueryContext(ctx, query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
templates := []dto.EmailTemplateDTO{}
|
|
for rows.Next() {
|
|
var t dto.EmailTemplateDTO
|
|
var varsJSON []byte
|
|
if err := rows.Scan(&t.ID, &t.Slug, &t.Subject, &t.BodyHTML, &varsJSON, &t.CreatedAt, &t.UpdatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(varsJSON) > 0 {
|
|
json.Unmarshal(varsJSON, &t.Variables)
|
|
}
|
|
templates = append(templates, t)
|
|
}
|
|
return templates, nil
|
|
}
|
|
|
|
func (s *AdminService) GetEmailTemplate(ctx context.Context, slug string) (*dto.EmailTemplateDTO, error) {
|
|
query := `SELECT id, slug, subject, body_html, variables, created_at, updated_at FROM email_templates WHERE slug = $1`
|
|
row := s.DB.QueryRowContext(ctx, query, slug)
|
|
|
|
t := &dto.EmailTemplateDTO{}
|
|
var varsJSON []byte
|
|
err := row.Scan(&t.ID, &t.Slug, &t.Subject, &t.BodyHTML, &varsJSON, &t.CreatedAt, &t.UpdatedAt)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
if len(varsJSON) > 0 {
|
|
json.Unmarshal(varsJSON, &t.Variables)
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
func (s *AdminService) CreateEmailTemplate(ctx context.Context, req dto.CreateEmailTemplateRequest) (*dto.EmailTemplateDTO, error) {
|
|
query := `INSERT INTO email_templates (slug, subject, body_html, variables, updated_at) VALUES ($1, $2, $3, $4, $5) RETURNING id, created_at`
|
|
|
|
varsJSON, _ := json.Marshal(req.Variables)
|
|
if varsJSON == nil {
|
|
varsJSON = []byte("[]")
|
|
}
|
|
|
|
t := &dto.EmailTemplateDTO{
|
|
Slug: req.Slug,
|
|
Subject: req.Subject,
|
|
BodyHTML: req.BodyHTML,
|
|
Variables: req.Variables,
|
|
}
|
|
err := s.DB.QueryRowContext(ctx, query, req.Slug, req.Subject, req.BodyHTML, varsJSON, time.Now()).Scan(&t.ID, &t.CreatedAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t.UpdatedAt = t.CreatedAt
|
|
return t, nil
|
|
}
|
|
|
|
func (s *AdminService) UpdateEmailTemplate(ctx context.Context, slug string, req dto.UpdateEmailTemplateRequest) (*dto.EmailTemplateDTO, error) {
|
|
// Fetch existing
|
|
existing, err := s.GetEmailTemplate(ctx, slug)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if existing == nil {
|
|
return nil, fmt.Errorf("template not found")
|
|
}
|
|
|
|
// Apply updates
|
|
if req.Subject != nil {
|
|
existing.Subject = *req.Subject
|
|
}
|
|
if req.BodyHTML != nil {
|
|
existing.BodyHTML = *req.BodyHTML
|
|
}
|
|
if req.Variables != nil {
|
|
existing.Variables = *req.Variables
|
|
}
|
|
|
|
varsJSON, _ := json.Marshal(existing.Variables)
|
|
query := `UPDATE email_templates SET subject=$1, body_html=$2, variables=$3, updated_at=$4 WHERE slug=$5`
|
|
_, err = s.DB.ExecContext(ctx, query, existing.Subject, existing.BodyHTML, varsJSON, time.Now(), slug)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return s.GetEmailTemplate(ctx, slug)
|
|
}
|
|
|
|
func (s *AdminService) DeleteEmailTemplate(ctx context.Context, slug string) error {
|
|
_, err := s.DB.ExecContext(ctx, "DELETE FROM email_templates WHERE slug=$1", slug)
|
|
return err
|
|
}
|
|
|
|
func (s *AdminService) GetEmailSettings(ctx context.Context) (*dto.EmailSettingsDTO, error) {
|
|
query := `SELECT id, provider, smtp_host, smtp_port, smtp_user, smtp_pass, smtp_secure, sender_name, sender_email, amqp_url, is_active, updated_at
|
|
FROM email_settings WHERE is_active = true ORDER BY updated_at DESC LIMIT 1`
|
|
|
|
row := s.DB.QueryRowContext(ctx, query)
|
|
var s_ dto.EmailSettingsDTO
|
|
|
|
err := row.Scan(
|
|
&s_.ID, &s_.Provider, &s_.SMTPHost, &s_.SMTPPort, &s_.SMTPUser, &s_.SMTPPass,
|
|
&s_.SMTPSecure, &s_.SenderName, &s_.SenderEmail, &s_.AMQPURL, &s_.IsActive, &s_.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
return &s_, nil
|
|
}
|
|
|
|
func (s *AdminService) UpdateEmailSettings(ctx context.Context, req dto.UpdateEmailSettingsRequest) (*dto.EmailSettingsDTO, error) {
|
|
existing, err := s.GetEmailSettings(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if existing == nil {
|
|
// Insert new
|
|
query := `INSERT INTO email_settings (provider, smtp_host, smtp_port, smtp_user, smtp_pass, smtp_secure, sender_name, sender_email, amqp_url, is_active)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, true) RETURNING id, updated_at`
|
|
newS := &dto.EmailSettingsDTO{
|
|
Provider: "smtp",
|
|
SMTPSecure: true,
|
|
SenderName: "GoHorse Jobs",
|
|
SenderEmail: "no-reply@gohorsejobs.com",
|
|
IsActive: true,
|
|
}
|
|
applyEmailSettingsUpdate(newS, req)
|
|
err = s.DB.QueryRowContext(ctx, query, newS.Provider, newS.SMTPHost, newS.SMTPPort, newS.SMTPUser, newS.SMTPPass, newS.SMTPSecure, newS.SenderName, newS.SenderEmail, newS.AMQPURL).Scan(&newS.ID, &newS.UpdatedAt)
|
|
return newS, err
|
|
}
|
|
|
|
// Update existing
|
|
applyEmailSettingsUpdate(existing, req)
|
|
query := `UPDATE email_settings SET provider=$1, smtp_host=$2, smtp_port=$3, smtp_user=$4, smtp_pass=$5, smtp_secure=$6, sender_name=$7, sender_email=$8, amqp_url=$9, updated_at=$10 WHERE id=$11`
|
|
_, err = s.DB.ExecContext(ctx, query, existing.Provider, existing.SMTPHost, existing.SMTPPort, existing.SMTPUser, existing.SMTPPass, existing.SMTPSecure, existing.SenderName, existing.SenderEmail, existing.AMQPURL, time.Now(), existing.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.GetEmailSettings(ctx)
|
|
}
|
|
|
|
func applyEmailSettingsUpdate(s *dto.EmailSettingsDTO, req dto.UpdateEmailSettingsRequest) {
|
|
if req.Provider != nil {
|
|
s.Provider = *req.Provider
|
|
}
|
|
if req.SMTPHost != nil {
|
|
s.SMTPHost = req.SMTPHost
|
|
}
|
|
if req.SMTPPort != nil {
|
|
s.SMTPPort = req.SMTPPort
|
|
}
|
|
if req.SMTPUser != nil {
|
|
s.SMTPUser = req.SMTPUser
|
|
}
|
|
if req.SMTPPass != nil {
|
|
s.SMTPPass = req.SMTPPass
|
|
}
|
|
if req.SMTPSecure != nil {
|
|
s.SMTPSecure = *req.SMTPSecure
|
|
}
|
|
if req.SenderName != nil {
|
|
s.SenderName = *req.SenderName
|
|
}
|
|
if req.SenderEmail != nil {
|
|
s.SenderEmail = *req.SenderEmail
|
|
}
|
|
if req.AMQPURL != nil {
|
|
s.AMQPURL = req.AMQPURL
|
|
}
|
|
}
|