diff --git a/backend/internal/handlers/job_handler.go b/backend/internal/handlers/job_handler.go index ead9e12..afc2753 100755 --- a/backend/internal/handlers/job_handler.go +++ b/backend/internal/handlers/job_handler.go @@ -60,6 +60,9 @@ func (h *JobHandler) GetJobs(w http.ResponseWriter, r *http.Request) { if search == "" { search = r.URL.Query().Get("q") } + if search == "" { + search = r.URL.Query().Get("s") // Careerjet-style alias (What) + } employmentType := r.URL.Query().Get("employmentType") if employmentType == "" { @@ -68,6 +71,9 @@ func (h *JobHandler) GetJobs(w http.ResponseWriter, r *http.Request) { workMode := r.URL.Query().Get("workMode") location := r.URL.Query().Get("location") + if location == "" { + location = r.URL.Query().Get("l") // Careerjet-style alias (Where) + } salaryMinStr := r.URL.Query().Get("salaryMin") salaryMaxStr := r.URL.Query().Get("salaryMax") sortBy := r.URL.Query().Get("sortBy") diff --git a/backend/internal/handlers/job_handler_test.go b/backend/internal/handlers/job_handler_test.go index 98d7fb4..d23be00 100644 --- a/backend/internal/handlers/job_handler_test.go +++ b/backend/internal/handlers/job_handler_test.go @@ -89,6 +89,32 @@ func TestGetJobs_Success(t *testing.T) { assert.Equal(t, 2, response.Pagination.Total) } + +func TestGetJobs_ParsesCareerjetAliases(t *testing.T) { + mockService := &mockJobService{ + getJobsFunc: func(filter dto.JobFilterQuery) ([]models.JobWithCompany, int, error) { + if assert.NotNil(t, filter.Search) { + assert.Equal(t, "software engineer", *filter.Search) + } + if assert.NotNil(t, filter.Location) { + assert.Equal(t, "Sao Paulo", *filter.Location) + } + if assert.NotNil(t, filter.LocationSearch) { + assert.Equal(t, "Sao Paulo", *filter.LocationSearch) + } + return []models.JobWithCompany{}, 0, nil + }, + } + + handler := NewJobHandler(mockService) + req := httptest.NewRequest("GET", "/jobs?s=software+engineer&l=Sao+Paulo", nil) + rr := httptest.NewRecorder() + + handler.GetJobs(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) +} + func TestCreateJob_Success(t *testing.T) { mockService := &mockJobService{ createJobFunc: func(req dto.CreateJobRequest, createdBy string) (*models.Job, error) { diff --git a/docs/CAREERJET_GAP_ANALYSIS.md b/docs/CAREERJET_GAP_ANALYSIS.md new file mode 100644 index 0000000..c538d73 --- /dev/null +++ b/docs/CAREERJET_GAP_ANALYSIS.md @@ -0,0 +1,91 @@ +# Gap Analysis: GoHorseJobs vs Careerjet (2026) + +## Objetivo +Mapear o que já existe no GoHorseJobs e o que ainda falta para alcançar um fluxo equivalente ao Careerjet, com backlog acionável. + +## Escopo analisado +- Busca pública de vagas (home + listagem + detalhe) +- Filtros e parâmetros de URL +- Recursos de candidato (alerta, salvar vaga, currículo) +- Recursos de empresa/recrutador + +## Achados principais (resumo executivo) + +### O que já está bom no GoHorseJobs +1. **Fluxo base de busca já existe** com listagem, paginação e filtros essenciais (`q`, localização, tipo, modalidade). +2. **Detalhe de vaga e candidatura** já cobrem o núcleo do funil candidato. +3. **Estrutura multi-perfil** (candidato/recrutador/admin) e módulo de empresas já está presente. + +### Lacunas mais críticas para paridade com Careerjet +1. **Compatibilidade de URL estilo Careerjet (`s`/`l`)** não estava completa de ponta a ponta. +2. **Filtros “Date posted”, empresa e jornada de trabalho** ainda não estão consolidados no fluxo público. +3. **Alertas de vaga por e-mail** ainda não estão evidentes no fluxo público principal. +4. **Página de empresas pública com foco em descoberta** precisa de reforço (seguir empresa, vagas da empresa com filtros rápidos). + +## Melhorias implementadas neste ciclo + +### 1) Compatibilidade de URL estilo Careerjet +- Home agora envia também os aliases `s` e `l` (além de `q` e `location`) para facilitar compartilhamento e importação de links externos. +- Home também envia `workMode` além de `mode` para reduzir inconsistência entre páginas. +- Página `/jobs` agora consome `s`/`l`/`mode`/`workMode` além dos parâmetros antigos. + +### 2) Correção de re-fetch para filtros avançados +- A listagem de vagas não reexecutava busca ao alterar alguns filtros avançados (salário, moeda, ordenação, suporte a visto). +- Ajustado para atualizar resultados ao mudar esses filtros. + +### 3) Backend com alias Careerjet +- Endpoint `GET /api/v1/jobs` passou a aceitar `s` como alias de termo de busca e `l` como alias de localização. +- Incluído teste automatizado cobrindo esse comportamento. + +## Backlog recomendado (priorizado) + +## P0 (alta prioridade, impacto imediato) +1. **Date Posted no backend + frontend** + - Backend: aceitar `datePosted` (`24h`, `7d`, `30d`) e filtrar por `created_at`. + - Frontend: filtro visível na listagem com UX similar ao Careerjet. +2. **Filtro por empresa no público** + - Endpoint de jobs com `companyId` já existe; falta UX forte para seleção por empresa. +3. **Persistência de buscas recentes** + - LocalStorage para anônimos + conta autenticada (sincronização opcional). + +## P1 (médio prazo) +4. **Alerta de vagas público** + - “Create alert” com confirmação por e-mail e gerenciamento no painel do candidato. +5. **Melhorias na página de empresas** + - Botão “seguir”, filtro por data e exibição de vagas com metadados completos. +6. **Salvar vaga** + - Reforçar botão “Salvar” em cards e detalhe para usuários autenticados. + +## P2 (evolução) +7. **Navegação Previous/Next no detalhe da vaga** +8. **Origem da vaga (source link) quando for agregação externa** +9. **Ranking de relevância para ordenação “relevance”** + +## Critérios de aceite sugeridos + +### Busca / URL +- Dado um link `/jobs?s=Data+Engineer&l=Recife`, a tela carrega com termo e local preenchidos e lista filtrada. +- Dado um link `/jobs?mode=remote`, o filtro de modalidade entra como “remote”. + +### Filtros avançados +- Ao alterar ordenação/moeda/faixa salarial, a lista atualiza sem refresh manual. + +### Backend +- `GET /api/v1/jobs?s=qa&l=Lisboa` retorna os mesmos resultados que `q` e `location` equivalentes. + +## Plano técnico (sprint recomendado de 2 semanas) + +### Semana 1 +- Implementar `datePosted` no backend e cobertura de testes. +- Expor filtro na UI pública e validar UX. +- Persistir “recent searches” localmente. + +### Semana 2 +- Criar fluxo de alertas (CRUD mínimo + envio inicial). +- Melhorar página pública de empresas (seguir + vagas + filtros). +- Ajustes de observabilidade (métricas de conversão busca → clique → candidatura). + +## Riscos e dependências +- **Dependência de modelagem de dados** para alertas e follows. +- **Qualidade dos dados de salário/empresa** impacta filtros e ordenação. +- **Internacionalização**: novas labels e mensagens precisam entrar no i18n. diff --git a/frontend/src/app/jobs/page.tsx b/frontend/src/app/jobs/page.tsx index 788e46f..e89d0c8 100644 --- a/frontend/src/app/jobs/page.tsx +++ b/frontend/src/app/jobs/page.tsx @@ -54,16 +54,29 @@ function JobsContent() { useEffect(() => { const tech = searchParams.get("tech") const q = searchParams.get("q") + const s = searchParams.get("s") const type = searchParams.get("type") + const location = searchParams.get("location") + const l = searchParams.get("l") + const mode = searchParams.get("mode") + const workMode = searchParams.get("workMode") - if (tech || q) { - setSearchTerm(tech || q || "") + if (tech || q || s) { + setSearchTerm(tech || q || s || "") + setShowFilters(true) + } + + if (location || l) { + setLocationFilter(location || l || "") setShowFilters(true) } if (type === "remote") { setWorkModeFilter("remote") setShowFilters(true) + } else if (mode || workMode) { + setWorkModeFilter(mode || workMode || "all") + setShowFilters(true) } }, [searchParams]) @@ -121,7 +134,19 @@ function JobsContent() { return () => { isMounted = false } - }, [currentPage, debouncedSearchTerm, debouncedLocation, typeFilter, workModeFilter]) + }, [ + currentPage, + debouncedSearchTerm, + debouncedLocation, + typeFilter, + workModeFilter, + salaryMin, + salaryMax, + currencyFilter, + visaSupport, + sortBy, + t, + ]) // Computed const totalPages = Math.ceil(totalJobs / ITEMS_PER_PAGE) diff --git a/frontend/src/components/home-search.tsx b/frontend/src/components/home-search.tsx index e83b3d0..28c00e7 100644 --- a/frontend/src/components/home-search.tsx +++ b/frontend/src/components/home-search.tsx @@ -20,9 +20,15 @@ export function HomeSearch() { const handleSearch = () => { const params = new URLSearchParams() if (searchTerm) params.set("q", searchTerm) + // Compatibilidade estilo Careerjet (What/Where => s/l) + if (searchTerm) params.set("s", searchTerm) if (location) params.set("location", location) + if (location) params.set("l", location) if (type && type !== "all") params.set("type", type) - if (workMode && workMode !== "all") params.set("mode", workMode) + if (workMode && workMode !== "all") { + params.set("mode", workMode) + params.set("workMode", workMode) + } if (salary) params.set("salary", salary) router.push(`/jobs?${params.toString()}`)