feat: add working hours and salary negotiable logic
Backend: - Updated DTOs to include SalaryNegotiable and WorkingHours - Updated JobService to map and persist these fields (CREATE, GET, UPDATE) - Ensure DB queries include new columns Frontend: - Added 'Working Hours' (Jornada de Trabalho) dropdown to PostJobPage - Updated state and submit logic - Improved salary display in confirmation step Seeder: - Updated jobs seeder to include salary_negotiable and valid working_hours
This commit is contained in:
parent
d6bb579260
commit
91e4417c95
4 changed files with 95 additions and 67 deletions
|
|
@ -9,6 +9,7 @@ type CreateJobRequest struct {
|
|||
SalaryMax *float64 `json:"salaryMax,omitempty"`
|
||||
SalaryType *string `json:"salaryType,omitempty" validate:"omitempty,oneof=hourly daily weekly monthly yearly"`
|
||||
Currency *string `json:"currency,omitempty" validate:"omitempty,oneof=BRL USD EUR GBP JPY"`
|
||||
SalaryNegotiable bool `json:"salaryNegotiable"`
|
||||
EmploymentType *string `json:"employmentType,omitempty" validate:"omitempty,oneof=full-time part-time dispatch contract temporary training voluntary permanent"`
|
||||
WorkingHours *string `json:"workingHours,omitempty"`
|
||||
Location *string `json:"location,omitempty"`
|
||||
|
|
@ -29,6 +30,7 @@ type UpdateJobRequest struct {
|
|||
SalaryMax *float64 `json:"salaryMax,omitempty"`
|
||||
SalaryType *string `json:"salaryType,omitempty" validate:"omitempty,oneof=hourly daily weekly monthly yearly"`
|
||||
Currency *string `json:"currency,omitempty" validate:"omitempty,oneof=BRL USD EUR GBP JPY"`
|
||||
SalaryNegotiable *bool `json:"salaryNegotiable,omitempty"`
|
||||
EmploymentType *string `json:"employmentType,omitempty" validate:"omitempty,oneof=full-time part-time dispatch contract temporary training voluntary permanent"`
|
||||
WorkingHours *string `json:"workingHours,omitempty"`
|
||||
Location *string `json:"location,omitempty"`
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ func (s *JobService) CreateJob(req dto.CreateJobRequest, createdBy string) (*mod
|
|||
INSERT INTO jobs (
|
||||
company_id, created_by, title, description, salary_min, salary_max, salary_type,
|
||||
employment_type, working_hours, location, region_id, city_id,
|
||||
requirements, benefits, visa_support, language_level, status, created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
|
||||
requirements, benefits, visa_support, language_level, status, 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)
|
||||
RETURNING id, created_at, updated_at
|
||||
`
|
||||
|
||||
|
|
@ -39,6 +39,7 @@ func (s *JobService) CreateJob(req dto.CreateJobRequest, createdBy string) (*mod
|
|||
SalaryMin: req.SalaryMin,
|
||||
SalaryMax: req.SalaryMax,
|
||||
SalaryType: req.SalaryType,
|
||||
SalaryNegotiable: req.SalaryNegotiable,
|
||||
EmploymentType: req.EmploymentType,
|
||||
WorkingHours: req.WorkingHours,
|
||||
Location: req.Location,
|
||||
|
|
@ -60,7 +61,7 @@ func (s *JobService) CreateJob(req dto.CreateJobRequest, createdBy string) (*mod
|
|||
query,
|
||||
job.CompanyID, job.CreatedBy, job.Title, job.Description, job.SalaryMin, job.SalaryMax, job.SalaryType,
|
||||
job.EmploymentType, job.WorkingHours, job.Location, job.RegionID, job.CityID,
|
||||
job.Requirements, job.Benefits, job.VisaSupport, job.LanguageLevel, job.Status, job.CreatedAt, job.UpdatedAt,
|
||||
job.Requirements, job.Benefits, job.VisaSupport, job.LanguageLevel, job.Status, job.CreatedAt, job.UpdatedAt, job.SalaryNegotiable,
|
||||
).Scan(&job.ID, &job.CreatedAt, &job.UpdatedAt)
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -76,7 +77,7 @@ func (s *JobService) GetJobs(filter dto.JobFilterQuery) ([]models.JobWithCompany
|
|||
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.location, j.status, j.is_featured, j.created_at, j.updated_at,
|
||||
j.employment_type, j.work_mode, j.working_hours, j.location, j.status, j.salary_negotiable, j.is_featured, j.created_at, j.updated_at,
|
||||
COALESCE(c.name, '') as company_name, c.logo_url as company_logo_url,
|
||||
r.name as region_name, ci.name as city_name
|
||||
FROM jobs j
|
||||
|
|
@ -217,7 +218,7 @@ func (s *JobService) GetJobs(filter dto.JobFilterQuery) ([]models.JobWithCompany
|
|||
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.Location, &j.Status, &j.IsFeatured, &j.CreatedAt, &j.UpdatedAt,
|
||||
&j.EmploymentType, &j.WorkMode, &j.WorkingHours, &j.Location, &j.Status, &j.SalaryNegotiable, &j.IsFeatured, &j.CreatedAt, &j.UpdatedAt,
|
||||
&j.CompanyName, &j.CompanyLogoURL, &j.RegionName, &j.CityName,
|
||||
); err != nil {
|
||||
return nil, 0, err
|
||||
|
|
@ -238,13 +239,14 @@ 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,
|
||||
employment_type, working_hours, location, region_id, city_id, salary_negotiable,
|
||||
requirements, benefits, visa_support, language_level, status, created_at, updated_at
|
||||
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.ID, &j.CompanyID, &j.Title, &j.Description, &j.SalaryMin, &j.SalaryMax, &j.SalaryType,
|
||||
&j.EmploymentType, &j.WorkingHours, &j.Location, &j.RegionID, &j.CityID, &j.SalaryNegotiable,
|
||||
&j.Requirements, &j.Benefits, &j.VisaSupport, &j.LanguageLevel, &j.Status, &j.CreatedAt, &j.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
|
|
@ -274,6 +276,11 @@ func (s *JobService) UpdateJob(id string, req dto.UpdateJobRequest) (*models.Job
|
|||
args = append(args, *req.Status)
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ export default function PostJobPage() {
|
|||
salaryFixed: "", // For fixed salary mode
|
||||
employmentType: "",
|
||||
workMode: "remote",
|
||||
workingHours: "",
|
||||
salaryNegotiable: false, // Candidate proposes salary
|
||||
});
|
||||
|
||||
|
|
@ -145,6 +146,7 @@ export default function PostJobPage() {
|
|||
salaryMax: job.salaryNegotiable ? null : (salaryMode === 'fixed' ? (job.salaryFixed ? parseInt(job.salaryFixed) : null) : (job.salaryMax ? parseInt(job.salaryMax) : null)),
|
||||
salaryNegotiable: job.salaryNegotiable,
|
||||
employmentType: job.employmentType || null,
|
||||
workingHours: job.workingHours || null,
|
||||
workMode: job.workMode,
|
||||
status: "pending", // Pending review
|
||||
}),
|
||||
|
|
@ -418,7 +420,7 @@ export default function PostJobPage() {
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<Label>Tipo de Contrato</Label>
|
||||
<select
|
||||
|
|
@ -434,6 +436,18 @@ export default function PostJobPage() {
|
|||
<option value="voluntary">Voluntário</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Jornada de Trabalho</Label>
|
||||
<select
|
||||
value={job.workingHours}
|
||||
onChange={(e) => setJob({ ...job, workingHours: e.target.value })}
|
||||
className="w-full px-3 py-2 border rounded-lg bg-background"
|
||||
>
|
||||
<option value="">Qualquer</option>
|
||||
<option value="full-time">Tempo Integral</option>
|
||||
<option value="part-time">Meio Período</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Modelo de Trabalho</Label>
|
||||
<select
|
||||
|
|
@ -482,7 +496,7 @@ export default function PostJobPage() {
|
|||
? (job.salaryFixed ? `R$ ${job.salaryFixed}` : "A combinar")
|
||||
: (job.salaryMin && job.salaryMax ? `R$ ${job.salaryMin} - R$ ${job.salaryMax}` : "A combinar")
|
||||
}</p>
|
||||
<p><strong>Tipo:</strong> {job.employmentType || "Qualquer"} / {job.workMode}</p>
|
||||
<p><strong>Tipo:</strong> {job.employmentType || "Qualquer"} / {job.workingHours === 'full-time' ? 'Integral' : job.workingHours === 'part-time' ? 'Meio Período' : 'Qualquer'} / {job.workMode}</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button variant="outline" onClick={() => setStep(2)} className="flex-1">
|
||||
|
|
|
|||
|
|
@ -103,30 +103,35 @@ export async function seedJobs() {
|
|||
const currency = currencies[i % currencies.length];
|
||||
const salaryType = salaryTypes[i % salaryTypes.length];
|
||||
|
||||
const workingHoursOptions = ['full-time', 'part-time', ''];
|
||||
const workingHours = workingHoursOptions[i % 3];
|
||||
const salaryNegotiable = i % 10 === 0; // 10% negotiable
|
||||
|
||||
// Prepare params for bulk insert
|
||||
jobParams.push(
|
||||
company.id,
|
||||
seedUserId,
|
||||
title,
|
||||
`We are looking for a talented ${title} to join our ${company.name} team. Your role as ${level} will be crucial.`,
|
||||
salaryMin,
|
||||
salaryMax,
|
||||
salaryNegotiable ? null : salaryMin, // If negotiable, min is null
|
||||
salaryNegotiable ? null : salaryMax, // If negotiable, max is null
|
||||
salaryType,
|
||||
currency,
|
||||
employmentType,
|
||||
workMode === 'remote' ? 'Flexible' : '9:00-18:00',
|
||||
workingHours,
|
||||
workMode === 'remote' ? 'Remote (Global)' : internationalLocations[i % internationalLocations.length],
|
||||
JSON.stringify(template.skills),
|
||||
JSON.stringify(getRandomItems(benefits, 4)),
|
||||
i % 5 === 0, // 20% offer visa support
|
||||
'beginner',
|
||||
'open',
|
||||
workMode
|
||||
workMode,
|
||||
salaryNegotiable
|
||||
);
|
||||
|
||||
// ($1, $2, $3, ...), ($18, $19, ...)
|
||||
const placeHolders = [];
|
||||
for (let k = 0; k < 17; k++) {
|
||||
for (let k = 0; k < 18; k++) {
|
||||
placeHolders.push(`$${paramCounter++}`);
|
||||
}
|
||||
jobValues.push(`(${placeHolders.join(',')})`);
|
||||
|
|
@ -139,7 +144,7 @@ export async function seedJobs() {
|
|||
const query = `
|
||||
INSERT INTO jobs (company_id, created_by, title, description,
|
||||
salary_min, salary_max, salary_type, currency, employment_type, working_hours,
|
||||
location, requirements, benefits, visa_support, language_level, status, work_mode)
|
||||
location, requirements, benefits, visa_support, language_level, status, work_mode, salary_negotiable)
|
||||
VALUES ${jobValues.join(',')}
|
||||
`;
|
||||
await pool.query(query, jobParams);
|
||||
|
|
|
|||
Loading…
Reference in a new issue