package services import ( "context" "database/sql" "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 countQuery := `SELECT COUNT(*) FROM companies` var countArgs []interface{} if verified != nil { countQuery += " WHERE 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 ` var args []interface{} if verified != nil { baseQuery += " WHERE 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 } 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) ([]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 int) (*dto.User, error) { query := ` SELECT id, name, email, role, created_at FROM users WHERE id = $1 ` var u dto.User var roleStr string if err := s.DB.QueryRowContext(ctx, query, id).Scan(&u.ID, &u.Name, &u.Email, &roleStr, &u.CreatedAt); err != nil { return nil, err } u.Role = roleStr return &u, nil } // GetCompanyByUserID fetches the company associated with a user func (s *AdminService) GetCompanyByUserID(ctx context.Context, userID int) (*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 %d", userID) } } return &c, nil }