feat(frontend): add work mode filter and randomize seeder types
This commit is contained in:
parent
78314f2b45
commit
640eb10703
6 changed files with 51 additions and 9 deletions
|
|
@ -19,6 +19,7 @@ type Job struct {
|
||||||
|
|
||||||
// Employment
|
// Employment
|
||||||
EmploymentType *string `json:"employmentType,omitempty" db:"employment_type"` // full-time, part-time, dispatch, contract
|
EmploymentType *string `json:"employmentType,omitempty" db:"employment_type"` // full-time, part-time, dispatch, contract
|
||||||
|
WorkMode *string `json:"workMode,omitempty" db:"work_mode"` // onsite, hybrid, remote
|
||||||
WorkingHours *string `json:"workingHours,omitempty" db:"working_hours"`
|
WorkingHours *string `json:"workingHours,omitempty" db:"working_hours"`
|
||||||
|
|
||||||
// Location
|
// Location
|
||||||
|
|
|
||||||
|
|
@ -64,10 +64,10 @@ func (s *JobService) CreateJob(req dto.CreateJobRequest) (*models.Job, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *JobService) GetJobs(filter dto.JobFilterQuery) ([]models.JobWithCompany, int, error) {
|
func (s *JobService) GetJobs(filter dto.JobFilterQuery) ([]models.JobWithCompany, int, error) {
|
||||||
baseQuery := `
|
baseQuery := `
|
||||||
SELECT
|
SELECT
|
||||||
j.id, j.company_id, j.title, j.description, j.salary_min, j.salary_max, j.salary_type,
|
j.id, j.company_id, j.title, j.description, j.salary_min, j.salary_max, j.salary_type,
|
||||||
j.employment_type, j.location, j.status, j.is_featured, j.created_at, j.updated_at,
|
j.employment_type, j.work_mode, j.location, j.status, j.is_featured, j.created_at, j.updated_at,
|
||||||
c.name as company_name, c.logo_url as company_logo_url,
|
c.name as company_name, c.logo_url as company_logo_url,
|
||||||
r.name as region_name, ci.name as city_name
|
r.name as region_name, ci.name as city_name
|
||||||
FROM jobs j
|
FROM jobs j
|
||||||
|
|
@ -75,7 +75,7 @@ func (s *JobService) GetJobs(filter dto.JobFilterQuery) ([]models.JobWithCompany
|
||||||
LEFT JOIN regions r ON j.region_id = r.id
|
LEFT JOIN regions r ON j.region_id = r.id
|
||||||
LEFT JOIN cities ci ON j.city_id = ci.id
|
LEFT JOIN cities ci ON j.city_id = ci.id
|
||||||
WHERE 1=1`
|
WHERE 1=1`
|
||||||
countQuery := `SELECT COUNT(*) FROM jobs j WHERE 1=1`
|
countQuery := `SELECT COUNT(*) FROM jobs j WHERE 1=1`
|
||||||
|
|
||||||
var args []interface{}
|
var args []interface{}
|
||||||
argId := 1
|
argId := 1
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ function JobsContent() {
|
||||||
const [searchTerm, setSearchTerm] = useState("")
|
const [searchTerm, setSearchTerm] = useState("")
|
||||||
const [locationFilter, setLocationFilter] = useState("all")
|
const [locationFilter, setLocationFilter] = useState("all")
|
||||||
const [typeFilter, setTypeFilter] = useState("all")
|
const [typeFilter, setTypeFilter] = useState("all")
|
||||||
|
const [workModeFilter, setWorkModeFilter] = useState("all")
|
||||||
const [sortBy, setSortBy] = useState("recent")
|
const [sortBy, setSortBy] = useState("recent")
|
||||||
const [showFilters, setShowFilters] = useState(false)
|
const [showFilters, setShowFilters] = useState(false)
|
||||||
|
|
||||||
|
|
@ -71,7 +72,7 @@ function JobsContent() {
|
||||||
// Reset page when filters change
|
// Reset page when filters change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentPage(1)
|
setCurrentPage(1)
|
||||||
}, [debouncedSearchTerm, locationFilter, typeFilter, sortBy])
|
}, [debouncedSearchTerm, locationFilter, typeFilter, workModeFilter, sortBy])
|
||||||
|
|
||||||
// Extrair valores únicos para os filtros
|
// Extrair valores únicos para os filtros
|
||||||
const uniqueLocations = useMemo(() => {
|
const uniqueLocations = useMemo(() => {
|
||||||
|
|
@ -84,6 +85,11 @@ function JobsContent() {
|
||||||
return Array.from(new Set(types))
|
return Array.from(new Set(types))
|
||||||
}, [jobs])
|
}, [jobs])
|
||||||
|
|
||||||
|
const uniqueWorkModes = useMemo(() => {
|
||||||
|
const modes = jobs.map(job => job.workMode).filter(Boolean) as string[]
|
||||||
|
return Array.from(new Set(modes))
|
||||||
|
}, [jobs])
|
||||||
|
|
||||||
const filteredAndSortedJobs = useMemo(() => {
|
const filteredAndSortedJobs = useMemo(() => {
|
||||||
let filtered = jobs.filter((job) => {
|
let filtered = jobs.filter((job) => {
|
||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
|
|
@ -92,8 +98,9 @@ function JobsContent() {
|
||||||
job.description.toLowerCase().includes(debouncedSearchTerm.toLowerCase())
|
job.description.toLowerCase().includes(debouncedSearchTerm.toLowerCase())
|
||||||
const matchesLocation = locationFilter === "all" || job.location.includes(locationFilter)
|
const matchesLocation = locationFilter === "all" || job.location.includes(locationFilter)
|
||||||
const matchesType = typeFilter === "all" || job.type === typeFilter
|
const matchesType = typeFilter === "all" || job.type === typeFilter
|
||||||
|
const matchesWorkMode = workModeFilter === "all" || job.workMode === workModeFilter
|
||||||
|
|
||||||
return matchesSearch && matchesLocation && matchesType
|
return matchesSearch && matchesLocation && matchesType && matchesWorkMode
|
||||||
})
|
})
|
||||||
|
|
||||||
// Ordenação
|
// Ordenação
|
||||||
|
|
@ -115,7 +122,7 @@ function JobsContent() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return filtered
|
return filtered
|
||||||
}, [debouncedSearchTerm, locationFilter, typeFilter, sortBy, jobs])
|
}, [debouncedSearchTerm, locationFilter, typeFilter, workModeFilter, sortBy, jobs])
|
||||||
|
|
||||||
// Pagination Logic
|
// Pagination Logic
|
||||||
const totalPages = Math.ceil(filteredAndSortedJobs.length / ITEMS_PER_PAGE)
|
const totalPages = Math.ceil(filteredAndSortedJobs.length / ITEMS_PER_PAGE)
|
||||||
|
|
@ -124,12 +131,13 @@ function JobsContent() {
|
||||||
currentPage * ITEMS_PER_PAGE
|
currentPage * ITEMS_PER_PAGE
|
||||||
)
|
)
|
||||||
|
|
||||||
const hasActiveFilters = searchTerm || locationFilter !== "all" || typeFilter !== "all"
|
const hasActiveFilters = searchTerm || locationFilter !== "all" || typeFilter !== "all" || workModeFilter !== "all"
|
||||||
|
|
||||||
const clearFilters = () => {
|
const clearFilters = () => {
|
||||||
setSearchTerm("")
|
setSearchTerm("")
|
||||||
setLocationFilter("all")
|
setLocationFilter("all")
|
||||||
setTypeFilter("all")
|
setTypeFilter("all")
|
||||||
|
setWorkModeFilter("all")
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -234,6 +242,23 @@ function JobsContent() {
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
|
<Select value={workModeFilter} onValueChange={setWorkModeFilter}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<MapPin className="h-4 w-4 mr-2" />
|
||||||
|
<SelectValue placeholder="Modalidade" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Todas as modalidades</SelectItem>
|
||||||
|
{uniqueWorkModes.map((mode) => (
|
||||||
|
<SelectItem key={mode} value={mode}>
|
||||||
|
{mode === "remote" ? "Remoto" :
|
||||||
|
mode === "hybrid" ? "Híbrido" :
|
||||||
|
mode === "onsite" ? "Presencial" : mode}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
<Select value={sortBy} onValueChange={setSortBy}>
|
<Select value={sortBy} onValueChange={setSortBy}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<ArrowUpDown className="h-4 w-4 mr-2" />
|
<ArrowUpDown className="h-4 w-4 mr-2" />
|
||||||
|
|
@ -297,6 +322,16 @@ function JobsContent() {
|
||||||
</button>
|
</button>
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
{workModeFilter !== "all" && (
|
||||||
|
<Badge variant="secondary" className="gap-1">
|
||||||
|
{workModeFilter === "remote" ? "Remoto" :
|
||||||
|
workModeFilter === "hybrid" ? "Híbrido" :
|
||||||
|
workModeFilter === "onsite" ? "Presencial" : workModeFilter}
|
||||||
|
<button onClick={() => setWorkModeFilter("all")} className="ml-1">
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,7 @@ export interface ApiJob {
|
||||||
salaryMax?: number;
|
salaryMax?: number;
|
||||||
salaryType?: string;
|
salaryType?: string;
|
||||||
employmentType?: string;
|
employmentType?: string;
|
||||||
|
workMode?: string;
|
||||||
workingHours?: string;
|
workingHours?: string;
|
||||||
location?: string;
|
location?: string;
|
||||||
regionId?: number;
|
regionId?: number;
|
||||||
|
|
@ -182,6 +183,7 @@ export function transformApiJobToFrontend(apiJob: ApiJob): import('./types').Job
|
||||||
company: apiJob.companyName || 'Empresa',
|
company: apiJob.companyName || 'Empresa',
|
||||||
location: apiJob.location || apiJob.cityName || 'Localização não informada',
|
location: apiJob.location || apiJob.cityName || 'Localização não informada',
|
||||||
type,
|
type,
|
||||||
|
workMode: apiJob.workMode as any,
|
||||||
salary,
|
salary,
|
||||||
description: apiJob.description,
|
description: apiJob.description,
|
||||||
requirements: requirements.length > 0 ? requirements : ['Ver detalhes'],
|
requirements: requirements.length > 0 ? requirements : ['Ver detalhes'],
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ export interface Job {
|
||||||
company: string;
|
company: string;
|
||||||
location: string;
|
location: string;
|
||||||
type: "full-time" | "part-time" | "contract" | "Remoto" | "Tempo Integral";
|
type: "full-time" | "part-time" | "contract" | "Remoto" | "Tempo Integral";
|
||||||
|
workMode?: "onsite" | "hybrid" | "remote";
|
||||||
salary?: string;
|
salary?: string;
|
||||||
description: string;
|
description: string;
|
||||||
requirements: string[];
|
requirements: string[];
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,9 @@ export async function seedJobs() {
|
||||||
const salaryMin = template.salaryRange[0] + getRandomInt(-2000, 2000);
|
const salaryMin = template.salaryRange[0] + getRandomInt(-2000, 2000);
|
||||||
const salaryMax = template.salaryRange[1] + getRandomInt(-2000, 3000);
|
const salaryMax = template.salaryRange[1] + getRandomInt(-2000, 3000);
|
||||||
|
|
||||||
|
const employmentTypes = ['full-time', 'part-time', 'contract'];
|
||||||
|
const employmentType = employmentTypes[i % employmentTypes.length]; // Deterministic variety
|
||||||
|
|
||||||
await pool.query(`
|
await pool.query(`
|
||||||
INSERT INTO jobs (company_id, created_by, title, description,
|
INSERT INTO jobs (company_id, created_by, title, description,
|
||||||
salary_min, salary_max, salary_type, employment_type, working_hours,
|
salary_min, salary_max, salary_type, employment_type, working_hours,
|
||||||
|
|
@ -98,11 +101,11 @@ export async function seedJobs() {
|
||||||
company.id,
|
company.id,
|
||||||
seedUserId,
|
seedUserId,
|
||||||
title,
|
title,
|
||||||
`We are looking for a talented ${title} to join our ${company.name} team. You will work on exciting projects and help drive our technical excellence.`,
|
`We are looking for a talented ${title} to join our ${company.name} team. Your role as ${level} will be crucial.`,
|
||||||
salaryMin,
|
salaryMin,
|
||||||
salaryMax,
|
salaryMax,
|
||||||
'monthly',
|
'monthly',
|
||||||
'full-time',
|
employmentType,
|
||||||
workMode === 'remote' ? 'Flexible' : '9:00-18:00',
|
workMode === 'remote' ? 'Flexible' : '9:00-18:00',
|
||||||
workMode === 'remote' ? 'Remote (Anywhere)' : 'São Paulo - SP',
|
workMode === 'remote' ? 'Remote (Anywhere)' : 'São Paulo - SP',
|
||||||
JSON.stringify(template.skills),
|
JSON.stringify(template.skills),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue