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
|
|
@ -2,43 +2,45 @@ package dto
|
||||||
|
|
||||||
// CreateJobRequest represents the request to create a new job
|
// CreateJobRequest represents the request to create a new job
|
||||||
type CreateJobRequest struct {
|
type CreateJobRequest struct {
|
||||||
CompanyID string `json:"companyId" validate:"required"`
|
CompanyID string `json:"companyId" validate:"required"`
|
||||||
Title string `json:"title" validate:"required,min=5,max=255"`
|
Title string `json:"title" validate:"required,min=5,max=255"`
|
||||||
Description string `json:"description" validate:"required,min=20"`
|
Description string `json:"description" validate:"required,min=20"`
|
||||||
SalaryMin *float64 `json:"salaryMin,omitempty"`
|
SalaryMin *float64 `json:"salaryMin,omitempty"`
|
||||||
SalaryMax *float64 `json:"salaryMax,omitempty"`
|
SalaryMax *float64 `json:"salaryMax,omitempty"`
|
||||||
SalaryType *string `json:"salaryType,omitempty" validate:"omitempty,oneof=hourly daily weekly monthly yearly"`
|
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"`
|
Currency *string `json:"currency,omitempty" validate:"omitempty,oneof=BRL USD EUR GBP JPY"`
|
||||||
EmploymentType *string `json:"employmentType,omitempty" validate:"omitempty,oneof=full-time part-time dispatch contract temporary training voluntary permanent"`
|
SalaryNegotiable bool `json:"salaryNegotiable"`
|
||||||
WorkingHours *string `json:"workingHours,omitempty"`
|
EmploymentType *string `json:"employmentType,omitempty" validate:"omitempty,oneof=full-time part-time dispatch contract temporary training voluntary permanent"`
|
||||||
Location *string `json:"location,omitempty"`
|
WorkingHours *string `json:"workingHours,omitempty"`
|
||||||
RegionID *int `json:"regionId,omitempty"`
|
Location *string `json:"location,omitempty"`
|
||||||
CityID *int `json:"cityId,omitempty"`
|
RegionID *int `json:"regionId,omitempty"`
|
||||||
Requirements map[string]interface{} `json:"requirements,omitempty"`
|
CityID *int `json:"cityId,omitempty"`
|
||||||
Benefits map[string]interface{} `json:"benefits,omitempty"`
|
Requirements map[string]interface{} `json:"requirements,omitempty"`
|
||||||
VisaSupport bool `json:"visaSupport"`
|
Benefits map[string]interface{} `json:"benefits,omitempty"`
|
||||||
LanguageLevel *string `json:"languageLevel,omitempty"`
|
VisaSupport bool `json:"visaSupport"`
|
||||||
Status string `json:"status" validate:"oneof=draft open closed review published paused expired archived reported"`
|
LanguageLevel *string `json:"languageLevel,omitempty"`
|
||||||
|
Status string `json:"status" validate:"oneof=draft open closed review published paused expired archived reported"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateJobRequest represents the request to update a job
|
// UpdateJobRequest represents the request to update a job
|
||||||
type UpdateJobRequest struct {
|
type UpdateJobRequest struct {
|
||||||
Title *string `json:"title,omitempty" validate:"omitempty,min=5,max=255"`
|
Title *string `json:"title,omitempty" validate:"omitempty,min=5,max=255"`
|
||||||
Description *string `json:"description,omitempty" validate:"omitempty,min=20"`
|
Description *string `json:"description,omitempty" validate:"omitempty,min=20"`
|
||||||
SalaryMin *float64 `json:"salaryMin,omitempty"`
|
SalaryMin *float64 `json:"salaryMin,omitempty"`
|
||||||
SalaryMax *float64 `json:"salaryMax,omitempty"`
|
SalaryMax *float64 `json:"salaryMax,omitempty"`
|
||||||
SalaryType *string `json:"salaryType,omitempty" validate:"omitempty,oneof=hourly daily weekly monthly yearly"`
|
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"`
|
Currency *string `json:"currency,omitempty" validate:"omitempty,oneof=BRL USD EUR GBP JPY"`
|
||||||
EmploymentType *string `json:"employmentType,omitempty" validate:"omitempty,oneof=full-time part-time dispatch contract temporary training voluntary permanent"`
|
SalaryNegotiable *bool `json:"salaryNegotiable,omitempty"`
|
||||||
WorkingHours *string `json:"workingHours,omitempty"`
|
EmploymentType *string `json:"employmentType,omitempty" validate:"omitempty,oneof=full-time part-time dispatch contract temporary training voluntary permanent"`
|
||||||
Location *string `json:"location,omitempty"`
|
WorkingHours *string `json:"workingHours,omitempty"`
|
||||||
RegionID *int `json:"regionId,omitempty"`
|
Location *string `json:"location,omitempty"`
|
||||||
CityID *int `json:"cityId,omitempty"`
|
RegionID *int `json:"regionId,omitempty"`
|
||||||
Requirements map[string]interface{} `json:"requirements,omitempty"`
|
CityID *int `json:"cityId,omitempty"`
|
||||||
Benefits map[string]interface{} `json:"benefits,omitempty"`
|
Requirements map[string]interface{} `json:"requirements,omitempty"`
|
||||||
VisaSupport *bool `json:"visaSupport,omitempty"`
|
Benefits map[string]interface{} `json:"benefits,omitempty"`
|
||||||
LanguageLevel *string `json:"languageLevel,omitempty"`
|
VisaSupport *bool `json:"visaSupport,omitempty"`
|
||||||
Status *string `json:"status,omitempty" validate:"omitempty,oneof=draft open closed review published paused expired archived reported"`
|
LanguageLevel *string `json:"languageLevel,omitempty"`
|
||||||
|
Status *string `json:"status,omitempty" validate:"omitempty,oneof=draft open closed review published paused expired archived reported"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateApplicationRequest represents a job application (guest or logged user)
|
// CreateApplicationRequest represents a job application (guest or logged user)
|
||||||
|
|
|
||||||
|
|
@ -26,31 +26,32 @@ func (s *JobService) CreateJob(req dto.CreateJobRequest, createdBy string) (*mod
|
||||||
INSERT INTO jobs (
|
INSERT INTO jobs (
|
||||||
company_id, created_by, title, description, salary_min, salary_max, salary_type,
|
company_id, created_by, 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,
|
||||||
requirements, benefits, visa_support, language_level, status, created_at, updated_at
|
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)
|
) 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
|
RETURNING id, created_at, updated_at
|
||||||
`
|
`
|
||||||
|
|
||||||
job := &models.Job{
|
job := &models.Job{
|
||||||
CompanyID: req.CompanyID,
|
CompanyID: req.CompanyID,
|
||||||
CreatedBy: createdBy,
|
CreatedBy: createdBy,
|
||||||
Title: req.Title,
|
Title: req.Title,
|
||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
SalaryMin: req.SalaryMin,
|
SalaryMin: req.SalaryMin,
|
||||||
SalaryMax: req.SalaryMax,
|
SalaryMax: req.SalaryMax,
|
||||||
SalaryType: req.SalaryType,
|
SalaryType: req.SalaryType,
|
||||||
EmploymentType: req.EmploymentType,
|
SalaryNegotiable: req.SalaryNegotiable,
|
||||||
WorkingHours: req.WorkingHours,
|
EmploymentType: req.EmploymentType,
|
||||||
Location: req.Location,
|
WorkingHours: req.WorkingHours,
|
||||||
RegionID: req.RegionID,
|
Location: req.Location,
|
||||||
CityID: req.CityID,
|
RegionID: req.RegionID,
|
||||||
Requirements: req.Requirements,
|
CityID: req.CityID,
|
||||||
Benefits: req.Benefits,
|
Requirements: req.Requirements,
|
||||||
VisaSupport: req.VisaSupport,
|
Benefits: req.Benefits,
|
||||||
LanguageLevel: req.LanguageLevel,
|
VisaSupport: req.VisaSupport,
|
||||||
Status: req.Status,
|
LanguageLevel: req.LanguageLevel,
|
||||||
CreatedAt: time.Now(),
|
Status: req.Status,
|
||||||
UpdatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("[JOB_SERVICE DEBUG] Executing INSERT query...")
|
fmt.Println("[JOB_SERVICE DEBUG] Executing INSERT query...")
|
||||||
|
|
@ -60,7 +61,7 @@ func (s *JobService) CreateJob(req dto.CreateJobRequest, createdBy string) (*mod
|
||||||
query,
|
query,
|
||||||
job.CompanyID, job.CreatedBy, job.Title, job.Description, job.SalaryMin, job.SalaryMax, job.SalaryType,
|
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.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)
|
).Scan(&job.ID, &job.CreatedAt, &job.UpdatedAt)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -76,7 +77,7 @@ func (s *JobService) GetJobs(filter dto.JobFilterQuery) ([]models.JobWithCompany
|
||||||
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.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,
|
COALESCE(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
|
||||||
|
|
@ -217,7 +218,7 @@ func (s *JobService) GetJobs(filter dto.JobFilterQuery) ([]models.JobWithCompany
|
||||||
var j models.JobWithCompany
|
var j models.JobWithCompany
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&j.ID, &j.CompanyID, &j.Title, &j.Description, &j.SalaryMin, &j.SalaryMax, &j.SalaryType,
|
&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,
|
&j.CompanyName, &j.CompanyLogoURL, &j.RegionName, &j.CityName,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
|
|
@ -238,13 +239,14 @@ func (s *JobService) GetJobByID(id string) (*models.Job, error) {
|
||||||
var j models.Job
|
var j models.Job
|
||||||
query := `
|
query := `
|
||||||
SELECT id, company_id, title, description, salary_min, salary_max, salary_type,
|
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
|
requirements, benefits, visa_support, language_level, status, created_at, updated_at
|
||||||
FROM jobs WHERE id = $1
|
FROM jobs WHERE id = $1
|
||||||
`
|
`
|
||||||
err := s.DB.QueryRow(query, id).Scan(
|
err := s.DB.QueryRow(query, id).Scan(
|
||||||
&j.ID, &j.CompanyID, &j.Title, &j.Description, &j.SalaryMin, &j.SalaryMax, &j.SalaryType,
|
&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,
|
&j.Requirements, &j.Benefits, &j.VisaSupport, &j.LanguageLevel, &j.Status, &j.CreatedAt, &j.UpdatedAt,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -274,6 +276,11 @@ func (s *JobService) UpdateJob(id string, req dto.UpdateJobRequest) (*models.Job
|
||||||
args = append(args, *req.Status)
|
args = append(args, *req.Status)
|
||||||
argId++
|
argId++
|
||||||
}
|
}
|
||||||
|
if req.SalaryNegotiable != nil {
|
||||||
|
setClauses = append(setClauses, fmt.Sprintf("salary_negotiable = $%d", argId))
|
||||||
|
args = append(args, *req.SalaryNegotiable)
|
||||||
|
argId++
|
||||||
|
}
|
||||||
|
|
||||||
if len(setClauses) == 0 {
|
if len(setClauses) == 0 {
|
||||||
return s.GetJobByID(id)
|
return s.GetJobByID(id)
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ export default function PostJobPage() {
|
||||||
salaryFixed: "", // For fixed salary mode
|
salaryFixed: "", // For fixed salary mode
|
||||||
employmentType: "",
|
employmentType: "",
|
||||||
workMode: "remote",
|
workMode: "remote",
|
||||||
|
workingHours: "",
|
||||||
salaryNegotiable: false, // Candidate proposes salary
|
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)),
|
salaryMax: job.salaryNegotiable ? null : (salaryMode === 'fixed' ? (job.salaryFixed ? parseInt(job.salaryFixed) : null) : (job.salaryMax ? parseInt(job.salaryMax) : null)),
|
||||||
salaryNegotiable: job.salaryNegotiable,
|
salaryNegotiable: job.salaryNegotiable,
|
||||||
employmentType: job.employmentType || null,
|
employmentType: job.employmentType || null,
|
||||||
|
workingHours: job.workingHours || null,
|
||||||
workMode: job.workMode,
|
workMode: job.workMode,
|
||||||
status: "pending", // Pending review
|
status: "pending", // Pending review
|
||||||
}),
|
}),
|
||||||
|
|
@ -418,7 +420,7 @@ export default function PostJobPage() {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label>Tipo de Contrato</Label>
|
<Label>Tipo de Contrato</Label>
|
||||||
<select
|
<select
|
||||||
|
|
@ -434,6 +436,18 @@ export default function PostJobPage() {
|
||||||
<option value="voluntary">Voluntário</option>
|
<option value="voluntary">Voluntário</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<Label>Modelo de Trabalho</Label>
|
<Label>Modelo de Trabalho</Label>
|
||||||
<select
|
<select
|
||||||
|
|
@ -482,7 +496,7 @@ export default function PostJobPage() {
|
||||||
? (job.salaryFixed ? `R$ ${job.salaryFixed}` : "A combinar")
|
? (job.salaryFixed ? `R$ ${job.salaryFixed}` : "A combinar")
|
||||||
: (job.salaryMin && job.salaryMax ? `R$ ${job.salaryMin} - R$ ${job.salaryMax}` : "A combinar")
|
: (job.salaryMin && job.salaryMax ? `R$ ${job.salaryMin} - R$ ${job.salaryMax}` : "A combinar")
|
||||||
}</p>
|
}</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>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Button variant="outline" onClick={() => setStep(2)} className="flex-1">
|
<Button variant="outline" onClick={() => setStep(2)} className="flex-1">
|
||||||
|
|
|
||||||
|
|
@ -103,30 +103,35 @@ export async function seedJobs() {
|
||||||
const currency = currencies[i % currencies.length];
|
const currency = currencies[i % currencies.length];
|
||||||
const salaryType = salaryTypes[i % salaryTypes.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
|
// Prepare params for bulk insert
|
||||||
jobParams.push(
|
jobParams.push(
|
||||||
company.id,
|
company.id,
|
||||||
seedUserId,
|
seedUserId,
|
||||||
title,
|
title,
|
||||||
`We are looking for a talented ${title} to join our ${company.name} team. Your role as ${level} will be crucial.`,
|
`We are looking for a talented ${title} to join our ${company.name} team. Your role as ${level} will be crucial.`,
|
||||||
salaryMin,
|
salaryNegotiable ? null : salaryMin, // If negotiable, min is null
|
||||||
salaryMax,
|
salaryNegotiable ? null : salaryMax, // If negotiable, max is null
|
||||||
salaryType,
|
salaryType,
|
||||||
currency,
|
currency,
|
||||||
employmentType,
|
employmentType,
|
||||||
workMode === 'remote' ? 'Flexible' : '9:00-18:00',
|
workingHours,
|
||||||
workMode === 'remote' ? 'Remote (Global)' : internationalLocations[i % internationalLocations.length],
|
workMode === 'remote' ? 'Remote (Global)' : internationalLocations[i % internationalLocations.length],
|
||||||
JSON.stringify(template.skills),
|
JSON.stringify(template.skills),
|
||||||
JSON.stringify(getRandomItems(benefits, 4)),
|
JSON.stringify(getRandomItems(benefits, 4)),
|
||||||
i % 5 === 0, // 20% offer visa support
|
i % 5 === 0, // 20% offer visa support
|
||||||
'beginner',
|
'beginner',
|
||||||
'open',
|
'open',
|
||||||
workMode
|
workMode,
|
||||||
|
salaryNegotiable
|
||||||
);
|
);
|
||||||
|
|
||||||
// ($1, $2, $3, ...), ($18, $19, ...)
|
// ($1, $2, $3, ...), ($18, $19, ...)
|
||||||
const placeHolders = [];
|
const placeHolders = [];
|
||||||
for (let k = 0; k < 17; k++) {
|
for (let k = 0; k < 18; k++) {
|
||||||
placeHolders.push(`$${paramCounter++}`);
|
placeHolders.push(`$${paramCounter++}`);
|
||||||
}
|
}
|
||||||
jobValues.push(`(${placeHolders.join(',')})`);
|
jobValues.push(`(${placeHolders.join(',')})`);
|
||||||
|
|
@ -139,7 +144,7 @@ export async function seedJobs() {
|
||||||
const query = `
|
const 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, currency, employment_type, working_hours,
|
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(',')}
|
VALUES ${jobValues.join(',')}
|
||||||
`;
|
`;
|
||||||
await pool.query(query, jobParams);
|
await pool.query(query, jobParams);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue