package http import ( "encoding/json" "fmt" "log/slog" "net/http" "strconv" "github.com/gorilla/mux" "github.com/gorilla/schema" "github.com/jobs-scraper/internal/utils" "github.com/jobs-scraper/libs/ports" "github.com/jobs-scraper/libs/repo" ) // JobHandler handles HTTP requests related to jobs type JobHandler struct { jobCommands *ports.JobCommands jobAnalysisResultRepo *repo.JobAnalysisResultRepository jobQueries *ports.JobQueries } // NewJobHandler creates a new instance of JobHandler func NewJobHandler(jobCommands *ports.JobCommands, jobAnalysisResultRepo *repo.JobAnalysisResultRepository, jobQueries *ports.JobQueries) *JobHandler { return &JobHandler{ jobCommands: jobCommands, jobAnalysisResultRepo: jobAnalysisResultRepo, jobQueries: jobQueries, } } var decoder = schema.NewDecoder() // RegisterRoutes registers all routes to the router func (h *JobHandler) RegisterRoutes(router *mux.Router) { jobs := router.PathPrefix("/jobs").Subrouter() //jobs.Use(AuthMiddleware) jobs.HandleFunc("", utils.Make(h.CreateJob)).Methods("POST") jobs.HandleFunc("/analysis", utils.Make(h.GetAllAnalysisResults)).Methods("GET") jobs.HandleFunc("/{id}/analysis", utils.Make(h.GetJobAnalysisResult)).Methods("GET") jobs.HandleFunc("/analysis/top-matches", utils.Make(h.GetTopMatches)).Methods("GET") jobs.HandleFunc("", utils.Make(h.GetJobs)).Methods("GET") jobs.HandleFunc("/apply/{id}", utils.Make(h.ApplyForJob)).Methods("POST") } // CreateJob handles the creation of a new job // @Summary Create a new job // @Description Creates a new job with the provided specifications // @Tags jobs // @Accept json // @Produce json // @Param job body ports.CreateJobCommand true "Job creation data" // @Success 201 {object} map[string]string "Successfully created job" // @Failure 400 {object} map[string]string "Invalid request data" // @Failure 500 {object} map[string]string "Internal server error" // @Router /jobs [post] func (h *JobHandler) CreateJob(w http.ResponseWriter, r *http.Request) error { var cmd ports.CreateJobCommand if err := json.NewDecoder(r.Body).Decode(&cmd); err != nil { slog.Error(err.Error()) return utils.NewAPIError(http.StatusBadRequest, utils.InvalidJSON()) } err := h.jobCommands.CreateJob.Handle(cmd) if err != nil { slog.Error(err.Error()) return utils.NewAPIError(http.StatusInternalServerError, err) } utils.WriteJSON(w, http.StatusCreated, map[string]string{"message": "Success"}) return nil } // Apply applies for a job by id // @Summary applies for a job // @Description Applies for a job with the provided id // @Tags jobs // @Param id query string false "Job ID" // @Accept json // @Produce json // @Success 201 {object} map[string]string "Successfully applied for job" // @Failure 400 {object} map[string]string "Invalid request data" // @Failure 500 {object} map[string]string "Internal server error" // @Router /jobs/apply/{id} [post] func (h *JobHandler) ApplyForJob(w http.ResponseWriter, r *http.Request) error { jobId := r.URL.Query().Get("id") if jobId == "" { slog.Error("Job ID is required") return utils.NewAPIError(http.StatusBadRequest, fmt.Errorf("job ID is required")) } jobIdInt, err := strconv.Atoi(jobId) if err != nil { slog.Error("Invalid Job ID", "error", err) return utils.NewAPIError(http.StatusBadRequest, fmt.Errorf("invalid job ID")) } err = h.jobCommands.ApplyForJob.Handle(jobIdInt) if err != nil { slog.Error(err.Error()) return utils.NewAPIError(http.StatusInternalServerError, fmt.Errorf("Internal server error")) } utils.WriteJSON(w, http.StatusCreated, map[string]string{"message": "Success"}) return nil } // GetJobs handles the retrieval of all jobs // @Summary Gets all jobs // @Description Gets all jobs with the provided specifications // @Tags jobs // @Param location query string false "Location filter" // @Param keywords query string false "Keywords filter (searches title and company)" // @Param fwt query string false "Full/Part time filter" // @Param provider query int false "Job provider (LinkedIn=0, Indeed=1, Glassdoor=2, Bayt=3, TokyoDev=4, JapanDev=5, Google=6, HiringCafe=7)" Enums(0,1,2,3,4,5,6,7) // @Param pageNumber query int false "Page number" // @Param pageSize query int false "Number of elements" // @Accept json // @Produce json // @Success 200 {object} map[string]any "Successfully retrieved jobs" // @Failure 400 {object} map[string]any "Invalid request data" // @Failure 500 {object} map[string]any "Internal server error" // @Router /jobs [get] func (h *JobHandler) GetJobs(w http.ResponseWriter, r *http.Request) error { query := ports.GetJobQuery{ PageNumber: 1, PageSize: 10, } if pageNumberStr := r.URL.Query().Get("pageNumber"); pageNumberStr != "" { if page, err := strconv.Atoi(pageNumberStr); err == nil && page > 0 { query.PageNumber = page } } if pageSizeStr := r.URL.Query().Get("pageSize"); pageSizeStr != "" { if pageSize, err := strconv.Atoi(pageSizeStr); err == nil && pageSize > 0 { query.PageSize = pageSize } } if err := decoder.Decode(&query, r.URL.Query()); err != nil { slog.Error("Failed to decode query params", "error", err) return utils.NewAPIError(http.StatusBadRequest, fmt.Errorf("invalid query parameters: %w", err)) } result, totalCount, err := h.jobQueries.GetJobs.Handle(query) if err != nil { slog.Error(err.Error()) return utils.NewAPIError(http.StatusInternalServerError, err) } pagination := utils.NewPageViewModel(totalCount, query.PageNumber, query.PageSize) utils.WriteJSON(w, http.StatusOK, map[string]any{ "jobs": result, "pagination": pagination, }) return nil } // GetAllAnalysisResults handles retrieving all job analysis results // @Summary Get all job analysis results // @Description Retrieves all job analysis results from the database // @Tags jobs // @Produce json // @Success 200 {array} domain.JobAnalysisResult "List of job analysis results" // @Failure 500 {object} map[string]string "Internal server error" // @Router /jobs/analysis [get] func (h *JobHandler) GetAllAnalysisResults(w http.ResponseWriter, r *http.Request) error { results, err := h.jobAnalysisResultRepo.GetAllAnalysisResults() if err != nil { slog.Error("Failed to get analysis results", "error", err) return utils.NewAPIError(http.StatusInternalServerError, err) } utils.WriteJSON(w, http.StatusOK, results) return nil } // GetJobAnalysisResult handles retrieving analysis result for a specific job // @Summary Get job analysis result by job ID // @Description Retrieves the analysis result for a specific job // @Tags jobs // @Produce json // @Param id path int true "Job ID" // @Success 200 {object} domain.JobAnalysisResult "Job analysis result" // @Failure 400 {object} map[string]string "Invalid job ID" // @Failure 404 {object} map[string]string "Analysis result not found" // @Failure 500 {object} map[string]string "Internal server error" // @Router /jobs/{id}/analysis [get] func (h *JobHandler) GetJobAnalysisResult(w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) jobID, err := strconv.ParseInt(vars["id"], 10, 64) if err != nil { return utils.NewAPIError(http.StatusBadRequest, fmt.Errorf("invalid job ID")) } result, err := h.jobAnalysisResultRepo.GetAnalysisResultByJobID(jobID) if err != nil { slog.Error("Failed to get analysis result", "jobID", jobID, "error", err) return utils.NewAPIError(http.StatusNotFound, fmt.Errorf("analysis result not found")) } utils.WriteJSON(w, http.StatusOK, result) return nil } // GetTopMatches handles retrieving top matching jobs based on analysis score // @Summary Get top matching jobs // @Description Retrieves jobs with high analysis match scores // @Tags jobs // @Produce json // @Param min_score query int false "Minimum match score (default: 70)" // @Success 200 {array} domain.JobAnalysisResult "List of top matching job analysis results" // @Failure 400 {object} map[string]string "Invalid minimum score" // @Failure 500 {object} map[string]string "Internal server error" // @Router /jobs/analysis/top-matches [get] func (h *JobHandler) GetTopMatches(w http.ResponseWriter, r *http.Request) error { minScoreStr := r.URL.Query().Get("min_score") minScore := 70 // default minimum score if minScoreStr != "" { var err error minScore, err = strconv.Atoi(minScoreStr) if err != nil { return utils.NewAPIError(http.StatusBadRequest, fmt.Errorf("invalid minimum score")) } } results, err := h.jobAnalysisResultRepo.GetAnalysisResultsByMatchScore(minScore) if err != nil { slog.Error("Failed to get top matches", "minScore", minScore, "error", err) return utils.NewAPIError(http.StatusInternalServerError, err) } utils.WriteJSON(w, http.StatusOK, results) return nil }