diff --git a/backend/internal/dto/requests.go b/backend/internal/dto/requests.go index dd09951..84f594e 100755 --- a/backend/internal/dto/requests.go +++ b/backend/internal/dto/requests.go @@ -7,8 +7,9 @@ type CreateJobRequest struct { Description string `json:"description" validate:"required,min=20"` SalaryMin *float64 `json:"salaryMin,omitempty"` SalaryMax *float64 `json:"salaryMax,omitempty"` - SalaryType *string `json:"salaryType,omitempty" validate:"omitempty,oneof=hourly monthly yearly"` - EmploymentType *string `json:"employmentType,omitempty" validate:"omitempty,oneof=full-time part-time dispatch contract"` + 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"` + 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"` RegionID *int `json:"regionId,omitempty"` @@ -26,8 +27,9 @@ type UpdateJobRequest struct { Description *string `json:"description,omitempty" validate:"omitempty,min=20"` SalaryMin *float64 `json:"salaryMin,omitempty"` SalaryMax *float64 `json:"salaryMax,omitempty"` - SalaryType *string `json:"salaryType,omitempty" validate:"omitempty,oneof=hourly monthly yearly"` - EmploymentType *string `json:"employmentType,omitempty" validate:"omitempty,oneof=full-time part-time dispatch contract"` + 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"` + 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"` RegionID *int `json:"regionId,omitempty"` diff --git a/backend/internal/models/job.go b/backend/internal/models/job.go index d904186..4d8f841 100755 --- a/backend/internal/models/job.go +++ b/backend/internal/models/job.go @@ -15,7 +15,8 @@ type Job struct { // Salary SalaryMin *float64 `json:"salaryMin,omitempty" db:"salary_min"` SalaryMax *float64 `json:"salaryMax,omitempty" db:"salary_max"` - SalaryType *string `json:"salaryType,omitempty" db:"salary_type"` // hourly, monthly, yearly + SalaryType *string `json:"salaryType,omitempty" db:"salary_type"` // hourly, daily, weekly, monthly, yearly + Currency *string `json:"currency,omitempty" db:"currency"` // BRL, USD, EUR, GBP, JPY // Employment EmploymentType *string `json:"employmentType,omitempty" db:"employment_type"` // full-time, part-time, dispatch, contract diff --git a/backend/migrations/018_add_currency_to_jobs.sql b/backend/migrations/018_add_currency_to_jobs.sql new file mode 100644 index 0000000..f4db9ae --- /dev/null +++ b/backend/migrations/018_add_currency_to_jobs.sql @@ -0,0 +1,21 @@ +-- Migration: Add currency to jobs and expand options +-- Description: Adds currency field and expands salary_type and employment_type options + +-- Add currency column +ALTER TABLE jobs ADD COLUMN IF NOT EXISTS currency VARCHAR(3) DEFAULT 'BRL'; + +-- Update salary_type constraint to include more options +ALTER TABLE jobs DROP CONSTRAINT IF EXISTS jobs_salary_type_check; +ALTER TABLE jobs ADD CONSTRAINT jobs_salary_type_check + CHECK (salary_type IN ('hourly', 'daily', 'weekly', 'monthly', 'yearly')); + +-- Update employment_type constraint to include more options +ALTER TABLE jobs DROP CONSTRAINT IF EXISTS jobs_employment_type_check; +ALTER TABLE jobs ADD CONSTRAINT jobs_employment_type_check + CHECK (employment_type IN ('full-time', 'part-time', 'contract', 'temporary', 'training', 'voluntary', 'dispatch', 'permanent')); + +-- Add index for currency filtering +CREATE INDEX IF NOT EXISTS idx_jobs_currency ON jobs(currency); + +-- Comments +COMMENT ON COLUMN jobs.currency IS 'Currency code (BRL, USD, EUR, GBP, JPY, etc.)'; diff --git a/docs/DATABASE.md b/docs/DATABASE.md index bf3e3d4..d1777a5 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -1,108 +1,279 @@ # Database Schema Documentation -This document outlines the database schema for the GoHorseJobs platform, based on the backend Go models. +This document outlines the database schema for the GoHorseJobs platform. + +## Entity Relationship Diagram + +```mermaid +erDiagram + users { + int id PK + varchar identifier UK + varchar password_hash + varchar role + varchar full_name + varchar language + boolean active + timestamp created_at + timestamp updated_at + timestamp last_login_at + } + + companies { + int id PK + varchar name + varchar slug UK + varchar type + varchar document + varchar address + int region_id FK + int city_id FK + varchar phone + varchar email + varchar website + varchar logo_url + text description + boolean active + boolean verified + timestamp created_at + timestamp updated_at + } + + jobs { + int id PK + int company_id FK + int created_by FK + varchar title + text description + decimal salary_min + decimal salary_max + varchar salary_type + varchar currency + varchar employment_type + varchar work_mode + varchar working_hours + varchar location + int region_id FK + int city_id FK + jsonb requirements + jsonb benefits + boolean visa_support + varchar language_level + varchar status + boolean is_featured + timestamp created_at + timestamp updated_at + } + + applications { + int id PK + int job_id FK + int user_id FK + varchar name + varchar email + varchar phone + varchar line_id + varchar whatsapp + text message + varchar resume_url + jsonb documents + varchar status + text notes + timestamp created_at + timestamp updated_at + } + + regions { + int id PK + varchar name + varchar country_code + varchar code + } + + cities { + int id PK + int region_id FK + varchar name + } + + user_companies { + int id PK + int user_id FK + int company_id FK + varchar role + jsonb permissions + } + + favorite_jobs { + int id PK + int user_id FK + int job_id FK + } + + users ||--o{ user_companies : "belongs to" + companies ||--o{ user_companies : "has members" + companies ||--o{ jobs : "posts" + users ||--o{ jobs : "creates" + jobs ||--o{ applications : "receives" + users ||--o{ applications : "submits" + regions ||--o{ cities : "contains" + regions ||--o{ companies : "located in" + cities ||--o{ companies : "located in" + regions ||--o{ jobs : "located in" + cities ||--o{ jobs : "located in" + users ||--o{ favorite_jobs : "saves" + jobs ||--o{ favorite_jobs : "saved by" +``` ## Core Tables ### Users (`users`) Represents system users including Candidates, Recruiters, and Admins. -- **id**: `INTEGER` (PK) -- **identifier**: `VARCHAR` (Unique, Email or Username) -- **password_hash**: `VARCHAR` -- **role**: `VARCHAR` (superadmin, companyAdmin, recruiter, jobSeeker) -- **full_name**: `VARCHAR` -- **language**: `VARCHAR` (Default: 'pt') -- **active**: `BOOLEAN` -- **created_at**, **updated_at**, **last_login_at**: `TIMESTAMP` + +| Column | Type | Description | +|--------|------|-------------| +| id | INTEGER | Primary key | +| identifier | VARCHAR | Unique, Email or Username | +| password_hash | VARCHAR | Bcrypt hash | +| role | VARCHAR | `superadmin`, `companyAdmin`, `recruiter`, `jobSeeker` | +| full_name | VARCHAR | Display name | +| language | VARCHAR | Default: 'pt' | +| active | BOOLEAN | Account status | +| created_at | TIMESTAMP | Creation time | +| updated_at | TIMESTAMP | Last update | +| last_login_at | TIMESTAMP | Last login | + +--- ### Companies (`companies`) Represents employer organizations. -- **id**: `INTEGER` (PK) -- **name**: `VARCHAR` -- **slug**: `VARCHAR` (Unique) -- **type**: `VARCHAR` -- **document**: `VARCHAR` (Houjin Bangou / CNPJ) -- **address**, **region_id**, **city_id**, **phone**, **email**, **website**: Contact info -- **logo_url**, **description**: Branding -- **active**, **verified**: `BOOLEAN` -- **created_at**, **updated_at**: `TIMESTAMP` + +| Column | Type | Description | +|--------|------|-------------| +| id | INTEGER | Primary key | +| name | VARCHAR | Company name | +| slug | VARCHAR | URL-friendly identifier | +| type | VARCHAR | Company type | +| document | VARCHAR | Houjin Bangou / CNPJ | +| address | VARCHAR | Physical address | +| region_id | INTEGER | FK → regions.id | +| city_id | INTEGER | FK → cities.id | +| phone, email, website | VARCHAR | Contact info | +| logo_url | VARCHAR | Logo image URL | +| description | TEXT | Company description | +| active | BOOLEAN | Active status | +| verified | BOOLEAN | Verification status | + +--- ### Jobs (`jobs`) Represents job postings. -- **id**: `INTEGER` (PK) -- **company_id**: `INTEGER` (FK -> companies.id) -- **created_by**: `INTEGER` (FK -> users.id) -- **title**: `VARCHAR` -- **description**: `TEXT` -- **salary_min**, **salary_max**: `DECIMAL` -- **salary_type**: `VARCHAR` (hourly, monthly, yearly) -- **employment_type**: `VARCHAR` (full-time, part-time, etc.) -- **work_mode**: `VARCHAR` (onsite, hybrid, remote) -- **location**, **region_id**, **city_id**: Location details -- **requirements**, **benefits**: `JSONB` -- **visa_support**: `BOOLEAN` -- **language_level**: `VARCHAR` -- **status**: `VARCHAR` (draft, published, closed, etc.) -- **is_featured**: `BOOLEAN` -- **created_at**, **updated_at**: `TIMESTAMP` + +| Column | Type | Description | +|--------|------|-------------| +| id | INTEGER | Primary key | +| company_id | INTEGER | FK → companies.id | +| created_by | INTEGER | FK → users.id | +| title | VARCHAR | Job title | +| description | TEXT | Full job description | +| salary_min | DECIMAL | Minimum salary | +| salary_max | DECIMAL | Maximum salary | +| salary_type | VARCHAR | `hourly`, `daily`, `weekly`, `monthly`, `yearly` | +| currency | VARCHAR | `BRL`, `USD`, `EUR`, `GBP`, `JPY` | +| employment_type | VARCHAR | `full-time`, `part-time`, `contract`, `temporary`, `training`, `voluntary`, `permanent`, `dispatch` | +| work_mode | VARCHAR | `onsite`, `hybrid`, `remote` | +| working_hours | VARCHAR | e.g. "9:00-18:00" | +| location | VARCHAR | Location string | +| region_id | INTEGER | FK → regions.id | +| city_id | INTEGER | FK → cities.id | +| requirements | JSONB | Skills/requirements array | +| benefits | JSONB | Benefits array | +| visa_support | BOOLEAN | Visa sponsorship | +| language_level | VARCHAR | `N5`, `N4`, `N3`, `N2`, `N1`, `beginner`, `none` | +| status | VARCHAR | `draft`, `open`, `closed`, `published`, `paused`, `expired`, `archived`, `reported`, `review` | +| is_featured | BOOLEAN | Featured job flag | + +--- ### Applications (`applications`) Represents job applications. -- **id**: `INTEGER` (PK) -- **job_id**: `INTEGER` (FK -> jobs.id) -- **user_id**: `INTEGER` (FK -> users.id, Nullable for guests) -- **name**, **email**, **phone**, **line_id**, **whatsapp**: Applicant info -- **message**: `TEXT` -- **resume_url**: `VARCHAR` -- **documents**: `JSONB` -- **status**: `VARCHAR` (pending, reviewed, hired, etc.) -- **notes**: `TEXT` -- **created_at**, **updated_at**: `TIMESTAMP` + +| Column | Type | Description | +|--------|------|-------------| +| id | INTEGER | Primary key | +| job_id | INTEGER | FK → jobs.id | +| user_id | INTEGER | FK → users.id (nullable for guests) | +| name | VARCHAR | Applicant name | +| email | VARCHAR | Contact email | +| phone | VARCHAR | Phone number | +| line_id | VARCHAR | LINE ID | +| whatsapp | VARCHAR | WhatsApp number | +| message | TEXT | Cover message | +| resume_url | VARCHAR | Resume file URL | +| documents | JSONB | Additional documents | +| status | VARCHAR | `pending`, `reviewed`, `shortlisted`, `rejected`, `hired` | +| notes | TEXT | Recruiter notes | + +--- ## Reference Tables ### Regions (`regions`) -- **id**: `INTEGER` (PK) -- **name**: `VARCHAR` -- **country_code**: `VARCHAR` -- **code**: `VARCHAR` +| Column | Type | Description | +|--------|------|-------------| +| id | INTEGER | Primary key | +| name | VARCHAR | Region name | +| country_code | VARCHAR | e.g. "JP", "BR" | +| code | VARCHAR | Region code | ### Cities (`cities`) -- **id**: `INTEGER` (PK) -- **region_id**: `INTEGER` (FK -> regions.id) -- **name**: `VARCHAR` +| Column | Type | Description | +|--------|------|-------------| +| id | INTEGER | Primary key | +| region_id | INTEGER | FK → regions.id | +| name | VARCHAR | City name | -### Tags (`tags`) -- **id**: `INTEGER` (PK) -- **name**: `VARCHAR` -- **category**: `VARCHAR` -- **active**: `BOOLEAN` +--- -## Relations & Logs +## Junction Tables ### User Companies (`user_companies`) -Maps Users to Companies (N:M). -- **id**: `INTEGER` (PK) -- **user_id**: `INTEGER` (FK -> users.id) -- **company_id**: `INTEGER` (FK -> companies.id) -- **role**: `VARCHAR` (companyAdmin, recruiter) -- **permissions**: `JSONB` +Maps users to companies (N:M relationship). + +| Column | Type | Description | +|--------|------|-------------| +| id | INTEGER | Primary key | +| user_id | INTEGER | FK → users.id | +| company_id | INTEGER | FK → companies.id | +| role | VARCHAR | `companyAdmin`, `recruiter` | +| permissions | JSONB | Custom permissions | ### Favorite Jobs (`favorite_jobs`) -- **id**: `INTEGER` (PK) -- **user_id**: `INTEGER` -- **job_id**: `INTEGER` +| Column | Type | Description | +|--------|------|-------------| +| id | INTEGER | Primary key | +| user_id | INTEGER | FK → users.id | +| job_id | INTEGER | FK → jobs.id | + +--- + +## Audit Tables ### Login Audits (`login_audits`) -- **id**: `INTEGER` (PK) -- **user_id**: `VARCHAR` -- **identifier**: `VARCHAR` -- **ip_address**, **user_agent**: Metadata -- **created_at**: `TIMESTAMP` +| Column | Type | Description | +|--------|------|-------------| +| id | INTEGER | Primary key | +| user_id | VARCHAR | User identifier | +| identifier | VARCHAR | Login identifier used | +| ip_address | VARCHAR | Client IP | +| user_agent | VARCHAR | Browser/client info | +| created_at | TIMESTAMP | Login time | ### Password Resets (`password_resets`) -- **id**: `INTEGER` (PK) -- **user_id**: `INTEGER` -- **token**: `VARCHAR` -- **expires_at**: `TIMESTAMP` -- **used**: `BOOLEAN` +| Column | Type | Description | +|--------|------|-------------| +| id | INTEGER | Primary key | +| user_id | INTEGER | FK → users.id | +| token | VARCHAR | Reset token | +| expires_at | TIMESTAMP | Token expiry | +| used | BOOLEAN | Token used status | diff --git a/frontend/src/app/dashboard/jobs/new/page.tsx b/frontend/src/app/dashboard/jobs/new/page.tsx index 35af57e..02326a3 100644 --- a/frontend/src/app/dashboard/jobs/new/page.tsx +++ b/frontend/src/app/dashboard/jobs/new/page.tsx @@ -36,6 +36,7 @@ export default function NewJobPage() { salaryMin: "", salaryMax: "", salaryType: "", + currency: "BRL", workingHours: "", }) @@ -89,6 +90,7 @@ export default function NewJobPage() { salaryMin: formData.salaryMin ? parseFloat(formData.salaryMin) : undefined, salaryMax: formData.salaryMax ? parseFloat(formData.salaryMax) : undefined, salaryType: formData.salaryType as CreateJobPayload['salaryType'] || undefined, + currency: formData.currency as CreateJobPayload['currency'] || undefined, workingHours: formData.workingHours || undefined, status: "published", } @@ -251,30 +253,51 @@ export default function NewJobPage() { /> -
+
- + + +
+
+
- + @@ -348,7 +371,7 @@ export default function NewJobPage() {

Salary

{formData.salaryMin || formData.salaryMax - ? `R$ ${formData.salaryMin || "?"} - R$ ${formData.salaryMax || "?"} ${formData.salaryType ? `(${formData.salaryType})` : ""}` + ? `${formData.currency} ${formData.salaryMin || "?"} - ${formData.salaryMax || "?"} ${formData.salaryType ? `(${formData.salaryType})` : ""}` : "-"}

diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index d3519b1..8c66528 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -290,8 +290,9 @@ export interface CreateJobPayload { description: string; salaryMin?: number; salaryMax?: number; - salaryType?: 'hourly' | 'monthly' | 'yearly'; - employmentType?: 'full-time' | 'part-time' | 'dispatch' | 'contract'; + salaryType?: 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly'; + currency?: 'BRL' | 'USD' | 'EUR' | 'GBP' | 'JPY'; + employmentType?: 'full-time' | 'part-time' | 'dispatch' | 'contract' | 'temporary' | 'training' | 'voluntary' | 'permanent'; workingHours?: string; location?: string; status: 'draft' | 'published' | 'open'; diff --git a/seeder-api/src/seeders/jobs.js b/seeder-api/src/seeders/jobs.js index 706df0e..e696807 100644 --- a/seeder-api/src/seeders/jobs.js +++ b/seeder-api/src/seeders/jobs.js @@ -93,15 +93,19 @@ export async function seedJobs() { const salaryMin = template.salaryRange[0] + getRandomInt(-2000, 2000); const salaryMax = template.salaryRange[1] + getRandomInt(-2000, 3000); - const employmentTypes = ['full-time', 'part-time', 'contract']; - const employmentType = employmentTypes[i % employmentTypes.length]; // Deterministic variety + const employmentTypes = ['full-time', 'part-time', 'contract', 'permanent', 'temporary', 'training']; + const currencies = ['BRL', 'BRL', 'BRL', 'BRL', 'BRL', 'BRL', 'BRL', 'USD', 'USD', 'EUR']; // 70% BRL, 20% USD, 10% EUR + const salaryTypes = ['hourly', 'daily', 'weekly', 'monthly', 'yearly']; + const employmentType = employmentTypes[i % employmentTypes.length]; + const currency = currencies[i % currencies.length]; + const salaryType = salaryTypes[i % salaryTypes.length]; const jobId = crypto.randomUUID(); await pool.query(` INSERT INTO jobs (id, company_id, created_by, title, description, - salary_min, salary_max, salary_type, 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) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) `, [ jobId, company.id, @@ -110,7 +114,8 @@ export async function seedJobs() { `We are looking for a talented ${title} to join our ${company.name} team. Your role as ${level} will be crucial.`, salaryMin, salaryMax, - 'monthly', + salaryType, + currency, employmentType, workMode === 'remote' ? 'Flexible' : '9:00-18:00', workMode === 'remote' ? 'Remote (Global)' : internationalLocations[i % internationalLocations.length],