package services import ( "database/sql" "fmt" "strings" "time" "github.com/rede5/gohorsejobs/backend/internal/dto" "github.com/rede5/gohorsejobs/backend/internal/models" ) type JobService struct { DB *sql.DB } func NewJobService(db *sql.DB) *JobService { return &JobService{DB: db} } func (s *JobService) CreateJob(req dto.CreateJobRequest, createdBy string) (*models.Job, error) { fmt.Println("[JOB_SERVICE DEBUG] === CreateJob Started ===") fmt.Printf("[JOB_SERVICE DEBUG] CompanyID=%s, CreatedBy=%s, Title=%s, Status=%s\n", req.CompanyID, createdBy, req.Title, req.Status) query := ` INSERT INTO jobs ( company_id, created_by, title, description, salary_min, salary_max, salary_type, currency, employment_type, working_hours, location, region_id, city_id, requirements, benefits, questions, visa_support, language_level, status, date_posted, created_at, updated_at, salary_negotiable ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23) RETURNING id, date_posted, created_at, updated_at ` job := &models.Job{ CompanyID: req.CompanyID, CreatedBy: createdBy, Title: req.Title, Description: req.Description, SalaryMin: req.SalaryMin, SalaryMax: req.SalaryMax, SalaryType: req.SalaryType, Currency: req.Currency, SalaryNegotiable: req.SalaryNegotiable, EmploymentType: req.EmploymentType, WorkingHours: req.WorkingHours, Location: req.Location, RegionID: req.RegionID, CityID: req.CityID, Requirements: models.JSONMap(req.Requirements), Benefits: models.JSONMap(req.Benefits), Questions: models.JSONMap(req.Questions), VisaSupport: req.VisaSupport, LanguageLevel: req.LanguageLevel, Status: req.Status, DatePosted: ptrTime(time.Now()), CreatedAt: time.Now(), UpdatedAt: time.Now(), } fmt.Println("[JOB_SERVICE DEBUG] Executing INSERT query...") fmt.Printf("[JOB_SERVICE DEBUG] Job struct: %+v\n", job) err := s.DB.QueryRow( query, job.CompanyID, job.CreatedBy, job.Title, job.Description, job.SalaryMin, job.SalaryMax, job.SalaryType, job.Currency, job.EmploymentType, job.WorkingHours, job.Location, job.RegionID, job.CityID, job.Requirements, job.Benefits, job.Questions, job.VisaSupport, job.LanguageLevel, job.Status, job.DatePosted, job.CreatedAt, job.UpdatedAt, job.SalaryNegotiable, ).Scan(&job.ID, &job.DatePosted, &job.CreatedAt, &job.UpdatedAt) if err != nil { fmt.Printf("[JOB_SERVICE ERROR] INSERT query failed: %v\n", err) return nil, err } fmt.Printf("[JOB_SERVICE DEBUG] Job created successfully! ID=%s\n", job.ID) return job, nil } func (s *JobService) GetJobs(filter dto.JobFilterQuery) ([]models.JobWithCompany, int, error) { // Merged Query: Includes both HEAD and dev fields baseQuery := ` SELECT j.id, j.company_id, j.title, j.description, j.salary_min, j.salary_max, j.salary_type, j.employment_type, j.work_mode, j.working_hours, j.location, j.status, j.salary_negotiable, j.is_featured, COALESCE(j.date_posted, j.created_at) AS date_posted, j.created_at, j.updated_at, 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, j.view_count, j.featured_until, (SELECT COUNT(*) FROM applications a WHERE a.job_id = j.id) as applications_count FROM jobs j LEFT JOIN companies c ON j.company_id::text = c.id::text LEFT JOIN states r ON j.region_id::text = r.id::text LEFT JOIN cities ci ON j.city_id::text = ci.id::text WHERE 1=1` countQuery := `SELECT COUNT(*) FROM jobs j WHERE 1=1` var args []interface{} argId := 1 // Search (merged logic) if filter.Search != nil && *filter.Search != "" { searchTerm := fmt.Sprintf("%%%s%%", *filter.Search) clause := fmt.Sprintf(" AND (j.title ILIKE $%d OR j.description ILIKE $%d OR c.name ILIKE $%d)", argId, argId, argId) baseQuery += clause countQuery += clause args = append(args, searchTerm) argId++ } // Company filter if filter.CompanyID != nil { baseQuery += fmt.Sprintf(" AND j.company_id = $%d", argId) countQuery += fmt.Sprintf(" AND j.company_id = $%d", argId) args = append(args, *filter.CompanyID) argId++ } // Region filter if filter.RegionID != nil { baseQuery += fmt.Sprintf(" AND j.region_id = $%d", argId) countQuery += fmt.Sprintf(" AND j.region_id = $%d", argId) args = append(args, *filter.RegionID) argId++ } // City filter if filter.CityID != nil { baseQuery += fmt.Sprintf(" AND j.city_id = $%d", argId) countQuery += fmt.Sprintf(" AND j.city_id = $%d", argId) args = append(args, *filter.CityID) argId++ } // Employment type filter if filter.EmploymentType != nil && *filter.EmploymentType != "" && *filter.EmploymentType != "all" { baseQuery += fmt.Sprintf(" AND j.employment_type = $%d", argId) countQuery += fmt.Sprintf(" AND j.employment_type = $%d", argId) args = append(args, *filter.EmploymentType) argId++ } // Work mode filter if filter.WorkMode != nil && *filter.WorkMode != "" && *filter.WorkMode != "all" { baseQuery += fmt.Sprintf(" AND j.work_mode = $%d", argId) countQuery += fmt.Sprintf(" AND j.work_mode = $%d", argId) args = append(args, *filter.WorkMode) argId++ } // Location filter (Partial Match) if filter.Location != nil && *filter.Location != "" && *filter.Location != "all" { locTerm := fmt.Sprintf("%%%s%%", *filter.Location) baseQuery += fmt.Sprintf(" AND j.location ILIKE $%d", argId) countQuery += fmt.Sprintf(" AND j.location ILIKE $%d", argId) args = append(args, locTerm) argId++ } // Support HEAD's LocationSearch explicitly if different if filter.LocationSearch != nil && *filter.LocationSearch != "" && (filter.Location == nil || *filter.Location != *filter.LocationSearch) { locTerm := fmt.Sprintf("%%%s%%", *filter.LocationSearch) baseQuery += fmt.Sprintf(" AND j.location ILIKE $%d", argId) countQuery += fmt.Sprintf(" AND j.location ILIKE $%d", argId) args = append(args, locTerm) argId++ } // Status filter if filter.Status != nil && *filter.Status != "" { baseQuery += fmt.Sprintf(" AND j.status = $%d", argId) countQuery += fmt.Sprintf(" AND j.status = $%d", argId) args = append(args, *filter.Status) argId++ } // Featured filter if filter.IsFeatured != nil { baseQuery += fmt.Sprintf(" AND j.is_featured = $%d", argId) countQuery += fmt.Sprintf(" AND j.is_featured = $%d", argId) args = append(args, *filter.IsFeatured) argId++ } // Visa support filter if filter.VisaSupport != nil { baseQuery += fmt.Sprintf(" AND j.visa_support = $%d", argId) countQuery += fmt.Sprintf(" AND j.visa_support = $%d", argId) args = append(args, *filter.VisaSupport) argId++ } // Language Level if filter.LanguageLevel != nil && *filter.LanguageLevel != "" && *filter.LanguageLevel != "all" { baseQuery += fmt.Sprintf(" AND j.language_level = $%d", argId) countQuery += fmt.Sprintf(" AND j.language_level = $%d", argId) args = append(args, *filter.LanguageLevel) argId++ } // Currency if filter.Currency != nil && *filter.Currency != "" && *filter.Currency != "all" { baseQuery += fmt.Sprintf(" AND j.currency = $%d", argId) countQuery += fmt.Sprintf(" AND j.currency = $%d", argId) args = append(args, *filter.Currency) argId++ } // Salary range filters if filter.SalaryMin != nil { baseQuery += fmt.Sprintf(" AND j.salary_min >= $%d", argId) countQuery += fmt.Sprintf(" AND j.salary_min >= $%d", argId) args = append(args, *filter.SalaryMin) argId++ } if filter.SalaryMax != nil { baseQuery += fmt.Sprintf(" AND j.salary_max <= $%d", argId) countQuery += fmt.Sprintf(" AND j.salary_max <= $%d", argId) args = append(args, *filter.SalaryMax) argId++ } if filter.SalaryType != nil && *filter.SalaryType != "" { baseQuery += fmt.Sprintf(" AND j.salary_type = $%d", argId) countQuery += fmt.Sprintf(" AND j.salary_type = $%d", argId) args = append(args, *filter.SalaryType) argId++ } // Date Posted filter (24h, 7d, 30d) if filter.DatePosted != nil && *filter.DatePosted != "" { var hours int switch *filter.DatePosted { case "24h": hours = 24 case "7d": hours = 24 * 7 case "30d": hours = 24 * 30 default: hours = 0 } if hours > 0 { cutoffTime := time.Now().Add(-time.Duration(hours) * time.Hour) baseQuery += fmt.Sprintf(" AND COALESCE(j.date_posted, j.created_at) >= $%d", argId) countQuery += fmt.Sprintf(" AND COALESCE(j.date_posted, j.created_at) >= $%d", argId) args = append(args, cutoffTime) argId++ } } // Sorting sortClause := " ORDER BY j.is_featured DESC, COALESCE(j.date_posted, j.created_at) DESC" // default if filter.SortBy != nil { switch *filter.SortBy { case "recent", "date": sortClause = " ORDER BY j.is_featured DESC, COALESCE(j.date_posted, j.created_at) DESC" case "salary", "salary_asc": sortClause = " ORDER BY j.salary_min ASC NULLS LAST" case "salary_desc": sortClause = " ORDER BY j.salary_max DESC NULLS LAST" case "relevance": sortClause = " ORDER BY j.is_featured DESC, j.created_at DESC" } } // Override sort order if explicit if filter.SortOrder != nil { if *filter.SortOrder == "asc" { // Rely on SortBy providing correct default or direction. } } baseQuery += sortClause // Pagination limit := filter.Limit if limit == 0 { limit = 10 } if limit > 100 { limit = 100 } offset := (filter.Page - 1) * limit if offset < 0 { offset = 0 } paginationQuery := baseQuery + fmt.Sprintf(" LIMIT $%d OFFSET $%d", argId, argId+1) paginationArgs := append(args, limit, offset) rows, err := s.DB.Query(paginationQuery, paginationArgs...) if err != nil { return nil, 0, err } defer rows.Close() jobs := []models.JobWithCompany{} for rows.Next() { var j models.JobWithCompany if err := rows.Scan( &j.ID, &j.CompanyID, &j.Title, &j.Description, &j.SalaryMin, &j.SalaryMax, &j.SalaryType, &j.EmploymentType, &j.WorkMode, &j.WorkingHours, &j.Location, &j.Status, &j.SalaryNegotiable, &j.IsFeatured, &j.DatePosted, &j.CreatedAt, &j.UpdatedAt, &j.CompanyName, &j.CompanyLogoURL, &j.RegionName, &j.CityName, &j.ViewCount, &j.FeaturedUntil, &j.ApplicationsCount, ); err != nil { return nil, 0, err } jobs = append(jobs, j) } var total int err = s.DB.QueryRow(countQuery, args...).Scan(&total) if err != nil { return nil, 0, err } return jobs, total, nil } func (s *JobService) GetJobByID(id string) (*models.Job, error) { var j models.Job query := ` SELECT id, company_id, title, description, salary_min, salary_max, salary_type, employment_type, working_hours, location, region_id, city_id, requirements, benefits, visa_support, language_level, status, is_featured, featured_until, view_count, date_posted, created_at, updated_at, salary_negotiable, currency, work_mode FROM jobs WHERE id = $1 ` err := s.DB.QueryRow(query, id).Scan( &j.ID, &j.CompanyID, &j.Title, &j.Description, &j.SalaryMin, &j.SalaryMax, &j.SalaryType, &j.EmploymentType, &j.WorkingHours, &j.Location, &j.RegionID, &j.CityID, &j.Requirements, &j.Benefits, &j.VisaSupport, &j.LanguageLevel, &j.Status, &j.IsFeatured, &j.FeaturedUntil, &j.ViewCount, &j.DatePosted, &j.CreatedAt, &j.UpdatedAt, &j.SalaryNegotiable, &j.Currency, &j.WorkMode, ) if err != nil { return nil, err } return &j, nil } func (s *JobService) UpdateJob(id string, req dto.UpdateJobRequest) (*models.Job, error) { var setClauses []string var args []interface{} argId := 1 if req.Title != nil { setClauses = append(setClauses, fmt.Sprintf("title = $%d", argId)) args = append(args, *req.Title) argId++ } if req.Description != nil { setClauses = append(setClauses, fmt.Sprintf("description = $%d", argId)) args = append(args, *req.Description) argId++ } if req.SalaryMin != nil { setClauses = append(setClauses, fmt.Sprintf("salary_min = $%d", argId)) args = append(args, *req.SalaryMin) argId++ } if req.SalaryMax != nil { setClauses = append(setClauses, fmt.Sprintf("salary_max = $%d", argId)) args = append(args, *req.SalaryMax) argId++ } if req.SalaryType != nil { setClauses = append(setClauses, fmt.Sprintf("salary_type = $%d", argId)) args = append(args, *req.SalaryType) argId++ } if req.Currency != nil { setClauses = append(setClauses, fmt.Sprintf("currency = $%d", argId)) args = append(args, *req.Currency) argId++ } if req.EmploymentType != nil { setClauses = append(setClauses, fmt.Sprintf("employment_type = $%d", argId)) args = append(args, *req.EmploymentType) argId++ } if req.WorkingHours != nil { setClauses = append(setClauses, fmt.Sprintf("working_hours = $%d", argId)) args = append(args, *req.WorkingHours) argId++ } if req.Location != nil { setClauses = append(setClauses, fmt.Sprintf("location = $%d", argId)) args = append(args, *req.Location) argId++ } if req.RegionID != nil { setClauses = append(setClauses, fmt.Sprintf("region_id = $%d", argId)) args = append(args, *req.RegionID) argId++ } if req.CityID != nil { setClauses = append(setClauses, fmt.Sprintf("city_id = $%d", argId)) args = append(args, *req.CityID) argId++ } if req.Requirements != nil { setClauses = append(setClauses, fmt.Sprintf("requirements = $%d", argId)) args = append(args, req.Requirements) argId++ } if req.Benefits != nil { setClauses = append(setClauses, fmt.Sprintf("benefits = $%d", argId)) args = append(args, req.Benefits) argId++ } if req.Questions != nil { setClauses = append(setClauses, fmt.Sprintf("questions = $%d", argId)) args = append(args, req.Questions) argId++ } if req.VisaSupport != nil { setClauses = append(setClauses, fmt.Sprintf("visa_support = $%d", argId)) args = append(args, *req.VisaSupport) argId++ } if req.LanguageLevel != nil { setClauses = append(setClauses, fmt.Sprintf("language_level = $%d", argId)) args = append(args, *req.LanguageLevel) argId++ } if req.Status != nil { setClauses = append(setClauses, fmt.Sprintf("status = $%d", argId)) args = append(args, *req.Status) argId++ } if req.IsFeatured != nil { setClauses = append(setClauses, fmt.Sprintf("is_featured = $%d", argId)) args = append(args, *req.IsFeatured) argId++ } if req.FeaturedUntil != nil { setClauses = append(setClauses, fmt.Sprintf("featured_until = $%d", argId)) parsedTime, err := time.Parse(time.RFC3339, *req.FeaturedUntil) if err == nil { args = append(args, parsedTime) } else { args = append(args, nil) } argId++ } if req.SalaryNegotiable != nil { setClauses = append(setClauses, fmt.Sprintf("salary_negotiable = $%d", argId)) args = append(args, *req.SalaryNegotiable) argId++ } if len(setClauses) == 0 { return s.GetJobByID(id) } setClauses = append(setClauses, "updated_at = NOW()") query := fmt.Sprintf("UPDATE jobs SET %s WHERE id = $%d RETURNING id, updated_at", strings.Join(setClauses, ", "), argId) args = append(args, id) var j models.Job err := s.DB.QueryRow(query, args...).Scan(&j.ID, &j.UpdatedAt) if err != nil { return nil, err } return s.GetJobByID(id) } func (s *JobService) DeleteJob(id string) error { _, err := s.DB.Exec("DELETE FROM jobs WHERE id = $1", id) return err } func ptrTime(t time.Time) *time.Time { return &t }