[TASK-006] feat: Add HR entities and DTOs

Add TypeORM entities for the HR module:
- Department: Organizational departments with hierarchy
- JobPosition: Job titles/positions
- Employee: Employee records with manager self-reference
- Contract: Employment contracts
- LeaveType: Configurable leave types
- LeaveAllocation: Employee leave allocations
- Leave: Leave requests

Add DTOs with class-validator:
- CreateEmployeeDto, UpdateEmployeeDto
- CreateContractDto, UpdateContractDto, ActivateContractDto, TerminateContractDto
- CreateLeaveDto, UpdateLeaveDto, ApproveLeaveDto, RejectLeaveDto
- Filter DTOs for all entities
- HrPaginationDto

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-01-26 18:45:27 -06:00
parent 8565056de3
commit 56f5663583
14 changed files with 2039 additions and 0 deletions

View File

@ -0,0 +1,218 @@
import {
IsString,
IsUUID,
IsOptional,
IsDateString,
IsEnum,
IsNumber,
IsPositive,
MaxLength,
MinLength,
Min,
Max,
} from 'class-validator';
import { ContractType, ContractStatus, WageType } from '../entities/contract.entity';
/**
* DTO for creating a new employment contract
*/
export class CreateContractDto {
@IsUUID()
companyId: string;
@IsUUID()
employeeId: string;
@IsString()
@MinLength(1)
@MaxLength(255)
name: string;
@IsOptional()
@IsString()
@MaxLength(50)
reference?: string;
@IsOptional()
@IsEnum(['permanent', 'temporary', 'contractor', 'internship', 'part_time'])
contractType?: ContractType;
@IsOptional()
@IsEnum(['draft', 'active', 'expired', 'terminated', 'cancelled'])
status?: ContractStatus;
@IsDateString()
dateStart: string;
@IsOptional()
@IsDateString()
dateEnd?: string;
@IsOptional()
@IsUUID()
jobPositionId?: string;
@IsOptional()
@IsUUID()
departmentId?: string;
// Compensation
@IsNumber()
@IsPositive()
wage: number;
@IsOptional()
@IsEnum(['hourly', 'daily', 'weekly', 'biweekly', 'monthly', 'annual'])
wageType?: WageType;
@IsOptional()
@IsString()
@MaxLength(3)
currency?: string;
// Schedule
@IsOptional()
@IsNumber()
@Min(0)
@Max(168)
hoursPerWeek?: number;
@IsOptional()
@IsString()
@MaxLength(50)
scheduleType?: string;
// Trial period
@IsOptional()
@IsNumber()
@Min(0)
@Max(12)
trialPeriodMonths?: number;
@IsOptional()
@IsDateString()
trialDateEnd?: string;
// Metadata
@IsOptional()
@IsString()
notes?: string;
@IsOptional()
@IsString()
documentUrl?: string;
}
/**
* DTO for updating an existing contract
*/
export class UpdateContractDto {
@IsOptional()
@IsString()
@MinLength(1)
@MaxLength(255)
name?: string;
@IsOptional()
@IsString()
@MaxLength(50)
reference?: string | null;
@IsOptional()
@IsEnum(['permanent', 'temporary', 'contractor', 'internship', 'part_time'])
contractType?: ContractType;
@IsOptional()
@IsEnum(['draft', 'active', 'expired', 'terminated', 'cancelled'])
status?: ContractStatus;
@IsOptional()
@IsDateString()
dateStart?: string;
@IsOptional()
@IsDateString()
dateEnd?: string | null;
@IsOptional()
@IsUUID()
jobPositionId?: string | null;
@IsOptional()
@IsUUID()
departmentId?: string | null;
// Compensation
@IsOptional()
@IsNumber()
@IsPositive()
wage?: number;
@IsOptional()
@IsEnum(['hourly', 'daily', 'weekly', 'biweekly', 'monthly', 'annual'])
wageType?: WageType;
@IsOptional()
@IsString()
@MaxLength(3)
currency?: string;
// Schedule
@IsOptional()
@IsNumber()
@Min(0)
@Max(168)
hoursPerWeek?: number | null;
@IsOptional()
@IsString()
@MaxLength(50)
scheduleType?: string | null;
// Trial period
@IsOptional()
@IsNumber()
@Min(0)
@Max(12)
trialPeriodMonths?: number | null;
@IsOptional()
@IsDateString()
trialDateEnd?: string | null;
// Metadata
@IsOptional()
@IsString()
notes?: string | null;
@IsOptional()
@IsString()
documentUrl?: string | null;
// Termination (only for termination flow)
@IsOptional()
@IsString()
terminationReason?: string;
}
/**
* DTO for activating a contract
*/
export class ActivateContractDto {
@IsOptional()
@IsDateString()
activatedAt?: string;
}
/**
* DTO for terminating a contract
*/
export class TerminateContractDto {
@IsString()
@MinLength(1)
terminationReason: string;
@IsOptional()
@IsDateString()
terminatedAt?: string;
}

View File

@ -0,0 +1,180 @@
import {
IsString,
IsUUID,
IsOptional,
IsDateString,
IsEmail,
IsEnum,
MaxLength,
MinLength,
} from 'class-validator';
import { EmployeeStatus } from '../entities/employee.entity';
/**
* DTO for creating a new employee
*/
export class CreateEmployeeDto {
@IsUUID()
companyId: string;
@IsString()
@MinLength(1)
@MaxLength(50)
employeeNumber: string;
@IsString()
@MinLength(1)
@MaxLength(100)
firstName: string;
@IsString()
@MinLength(1)
@MaxLength(100)
lastName: string;
@IsOptional()
@IsUUID()
userId?: string;
@IsOptional()
@IsUUID()
partnerId?: string;
@IsOptional()
@IsUUID()
departmentId?: string;
@IsOptional()
@IsUUID()
jobPositionId?: string;
@IsOptional()
@IsUUID()
managerId?: string;
// Work contact
@IsOptional()
@IsEmail()
@MaxLength(255)
workEmail?: string;
@IsOptional()
@IsString()
@MaxLength(50)
workPhone?: string;
@IsOptional()
@IsString()
@MaxLength(50)
workMobile?: string;
@IsOptional()
@IsString()
@MaxLength(255)
workLocation?: string;
// Personal data
@IsOptional()
@IsEmail()
@MaxLength(255)
personalEmail?: string;
@IsOptional()
@IsString()
@MaxLength(50)
personalPhone?: string;
@IsOptional()
@IsDateString()
birthDate?: string;
@IsOptional()
@IsString()
@MaxLength(20)
gender?: string;
@IsOptional()
@IsString()
@MaxLength(20)
maritalStatus?: string;
// Identification
@IsOptional()
@IsString()
@MaxLength(50)
identificationType?: string;
@IsOptional()
@IsString()
@MaxLength(100)
identificationNumber?: string;
@IsOptional()
@IsString()
@MaxLength(50)
taxId?: string;
@IsOptional()
@IsString()
@MaxLength(50)
socialSecurityNumber?: string;
// Address
@IsOptional()
@IsString()
@MaxLength(255)
street?: string;
@IsOptional()
@IsString()
@MaxLength(100)
city?: string;
@IsOptional()
@IsString()
@MaxLength(100)
state?: string;
@IsOptional()
@IsString()
@MaxLength(20)
postalCode?: string;
@IsOptional()
@IsString()
@MaxLength(100)
country?: string;
// Employment
@IsDateString()
hireDate: string;
@IsOptional()
@IsEnum(['active', 'inactive', 'on_leave', 'terminated'])
status?: EmployeeStatus;
// Emergency contact
@IsOptional()
@IsString()
@MaxLength(255)
emergencyContactName?: string;
@IsOptional()
@IsString()
@MaxLength(50)
emergencyContactPhone?: string;
@IsOptional()
@IsString()
@MaxLength(50)
emergencyContactRelationship?: string;
// Metadata
@IsOptional()
@IsString()
notes?: string;
@IsOptional()
@IsString()
avatarUrl?: string;
}

View File

@ -0,0 +1,267 @@
import {
IsString,
IsUUID,
IsOptional,
IsDateString,
IsEnum,
IsNumber,
IsBoolean,
IsPositive,
MaxLength,
MinLength,
} from 'class-validator';
import { LeaveStatus, HalfDayType } from '../entities/leave.entity';
/**
* DTO for creating a new leave request
*/
export class CreateLeaveDto {
@IsUUID()
companyId: string;
@IsUUID()
employeeId: string;
@IsUUID()
leaveTypeId: string;
@IsOptional()
@IsUUID()
allocationId?: string;
@IsDateString()
dateFrom: string;
@IsDateString()
dateTo: string;
@IsNumber()
@IsPositive()
daysRequested: number;
@IsOptional()
@IsBoolean()
isHalfDay?: boolean;
@IsOptional()
@IsEnum(['morning', 'afternoon'])
halfDayType?: HalfDayType;
@IsOptional()
@IsEnum(['draft', 'submitted', 'approved', 'rejected', 'cancelled'])
status?: LeaveStatus;
@IsOptional()
@IsString()
requestReason?: string;
@IsOptional()
@IsString()
documentUrl?: string;
@IsOptional()
@IsString()
notes?: string;
}
/**
* DTO for updating an existing leave request
*/
export class UpdateLeaveDto {
@IsOptional()
@IsUUID()
leaveTypeId?: string;
@IsOptional()
@IsUUID()
allocationId?: string | null;
@IsOptional()
@IsDateString()
dateFrom?: string;
@IsOptional()
@IsDateString()
dateTo?: string;
@IsOptional()
@IsNumber()
@IsPositive()
daysRequested?: number;
@IsOptional()
@IsBoolean()
isHalfDay?: boolean;
@IsOptional()
@IsEnum(['morning', 'afternoon'])
halfDayType?: HalfDayType | null;
@IsOptional()
@IsEnum(['draft', 'submitted', 'approved', 'rejected', 'cancelled'])
status?: LeaveStatus;
@IsOptional()
@IsString()
requestReason?: string | null;
@IsOptional()
@IsString()
documentUrl?: string | null;
@IsOptional()
@IsString()
notes?: string | null;
}
/**
* DTO for submitting a leave request for approval
*/
export class SubmitLeaveDto {
@IsOptional()
@IsString()
requestReason?: string;
}
/**
* DTO for approving a leave request
*/
export class ApproveLeaveDto {
@IsOptional()
@IsString()
notes?: string;
}
/**
* DTO for rejecting a leave request
*/
export class RejectLeaveDto {
@IsString()
@MinLength(1)
@MaxLength(1000)
rejectionReason: string;
}
/**
* DTO for cancelling a leave request
*/
export class CancelLeaveDto {
@IsOptional()
@IsString()
@MaxLength(1000)
reason?: string;
}
/**
* DTO for creating a leave type
*/
export class CreateLeaveTypeDto {
@IsUUID()
companyId: string;
@IsString()
@MinLength(1)
@MaxLength(255)
name: string;
@IsString()
@MinLength(1)
@MaxLength(50)
code: string;
@IsOptional()
@IsString()
description?: string;
@IsOptional()
@IsString()
@MaxLength(7)
color?: string;
@IsOptional()
@IsEnum(['vacation', 'sick', 'personal', 'maternity', 'paternity', 'bereavement', 'unpaid', 'other'])
leaveCategory?: string;
@IsOptional()
@IsEnum(['fixed', 'accrual', 'unlimited'])
allocationType?: string;
@IsOptional()
@IsBoolean()
requiresApproval?: boolean;
@IsOptional()
@IsBoolean()
requiresDocument?: boolean;
@IsOptional()
@IsNumber()
maxDaysPerRequest?: number;
@IsOptional()
@IsNumber()
maxDaysPerYear?: number;
@IsOptional()
@IsNumber()
minDaysNotice?: number;
@IsOptional()
@IsBoolean()
isPaid?: boolean;
@IsOptional()
@IsNumber()
payPercentage?: number;
@IsOptional()
@IsBoolean()
isActive?: boolean;
}
/**
* DTO for creating a leave allocation
*/
export class CreateLeaveAllocationDto {
@IsUUID()
employeeId: string;
@IsUUID()
leaveTypeId: string;
@IsNumber()
@IsPositive()
daysAllocated: number;
@IsDateString()
dateFrom: string;
@IsDateString()
dateTo: string;
@IsOptional()
@IsString()
notes?: string;
}
/**
* DTO for updating a leave allocation
*/
export class UpdateLeaveAllocationDto {
@IsOptional()
@IsNumber()
@IsPositive()
daysAllocated?: number;
@IsOptional()
@IsDateString()
dateFrom?: string;
@IsOptional()
@IsDateString()
dateTo?: string;
@IsOptional()
@IsString()
notes?: string | null;
}

View File

@ -0,0 +1,260 @@
import {
IsString,
IsUUID,
IsOptional,
IsDateString,
IsEnum,
IsBoolean,
IsNumber,
Min,
} from 'class-validator';
import { EmployeeStatus } from '../entities/employee.entity';
import { ContractStatus, ContractType } from '../entities/contract.entity';
import { LeaveStatus } from '../entities/leave.entity';
import { LeaveTypeCategory } from '../entities/leave-type.entity';
/**
* Filters for querying employees
*/
export class EmployeeFiltersDto {
@IsOptional()
@IsUUID()
companyId?: string;
@IsOptional()
@IsUUID()
departmentId?: string;
@IsOptional()
@IsUUID()
jobPositionId?: string;
@IsOptional()
@IsUUID()
managerId?: string;
@IsOptional()
@IsEnum(['active', 'inactive', 'on_leave', 'terminated'])
status?: EmployeeStatus;
@IsOptional()
@IsBoolean()
hasUser?: boolean;
@IsOptional()
@IsString()
search?: string;
@IsOptional()
@IsDateString()
hireDateFrom?: string;
@IsOptional()
@IsDateString()
hireDateTo?: string;
}
/**
* Filters for querying departments
*/
export class DepartmentFiltersDto {
@IsOptional()
@IsUUID()
companyId?: string;
@IsOptional()
@IsUUID()
parentId?: string;
@IsOptional()
@IsBoolean()
isActive?: boolean;
@IsOptional()
@IsBoolean()
isRoot?: boolean;
@IsOptional()
@IsString()
search?: string;
}
/**
* Filters for querying job positions
*/
export class JobPositionFiltersDto {
@IsOptional()
@IsUUID()
companyId?: string;
@IsOptional()
@IsUUID()
departmentId?: string;
@IsOptional()
@IsBoolean()
isActive?: boolean;
@IsOptional()
@IsString()
search?: string;
}
/**
* Filters for querying contracts
*/
export class ContractFiltersDto {
@IsOptional()
@IsUUID()
companyId?: string;
@IsOptional()
@IsUUID()
employeeId?: string;
@IsOptional()
@IsEnum(['permanent', 'temporary', 'contractor', 'internship', 'part_time'])
contractType?: ContractType;
@IsOptional()
@IsEnum(['draft', 'active', 'expired', 'terminated', 'cancelled'])
status?: ContractStatus;
@IsOptional()
@IsDateString()
dateStartFrom?: string;
@IsOptional()
@IsDateString()
dateStartTo?: string;
@IsOptional()
@IsDateString()
dateEndFrom?: string;
@IsOptional()
@IsDateString()
dateEndTo?: string;
@IsOptional()
@IsBoolean()
expiringWithinDays?: number;
@IsOptional()
@IsString()
search?: string;
}
/**
* Filters for querying leave types
*/
export class LeaveTypeFiltersDto {
@IsOptional()
@IsUUID()
companyId?: string;
@IsOptional()
@IsEnum(['vacation', 'sick', 'personal', 'maternity', 'paternity', 'bereavement', 'unpaid', 'other'])
leaveCategory?: LeaveTypeCategory;
@IsOptional()
@IsBoolean()
isActive?: boolean;
@IsOptional()
@IsBoolean()
isPaid?: boolean;
@IsOptional()
@IsBoolean()
requiresApproval?: boolean;
@IsOptional()
@IsString()
search?: string;
}
/**
* Filters for querying leaves
*/
export class LeaveFiltersDto {
@IsOptional()
@IsUUID()
companyId?: string;
@IsOptional()
@IsUUID()
employeeId?: string;
@IsOptional()
@IsUUID()
leaveTypeId?: string;
@IsOptional()
@IsUUID()
approverId?: string;
@IsOptional()
@IsEnum(['draft', 'submitted', 'approved', 'rejected', 'cancelled'])
status?: LeaveStatus;
@IsOptional()
@IsDateString()
dateFrom?: string;
@IsOptional()
@IsDateString()
dateTo?: string;
@IsOptional()
@IsBoolean()
pendingApproval?: boolean;
@IsOptional()
@IsString()
search?: string;
}
/**
* Filters for querying leave allocations
*/
export class LeaveAllocationFiltersDto {
@IsOptional()
@IsUUID()
employeeId?: string;
@IsOptional()
@IsUUID()
leaveTypeId?: string;
@IsOptional()
@IsDateString()
asOfDate?: string;
@IsOptional()
@IsBoolean()
hasRemainingDays?: boolean;
}
/**
* Pagination parameters for HR queries
*/
export class HrPaginationDto {
@IsOptional()
@IsNumber()
@Min(1)
page?: number;
@IsOptional()
@IsNumber()
@Min(1)
limit?: number;
@IsOptional()
@IsString()
sortBy?: string;
@IsOptional()
@IsEnum(['asc', 'desc', 'ASC', 'DESC'])
sortOrder?: 'asc' | 'desc' | 'ASC' | 'DESC';
}

View File

@ -0,0 +1,29 @@
export { CreateEmployeeDto } from './create-employee.dto';
export { UpdateEmployeeDto } from './update-employee.dto';
export {
CreateContractDto,
UpdateContractDto,
ActivateContractDto,
TerminateContractDto,
} from './create-contract.dto';
export {
CreateLeaveDto,
UpdateLeaveDto,
SubmitLeaveDto,
ApproveLeaveDto,
RejectLeaveDto,
CancelLeaveDto,
CreateLeaveTypeDto,
CreateLeaveAllocationDto,
UpdateLeaveAllocationDto,
} from './create-leave.dto';
export {
EmployeeFiltersDto,
DepartmentFiltersDto,
JobPositionFiltersDto,
ContractFiltersDto,
LeaveTypeFiltersDto,
LeaveFiltersDto,
LeaveAllocationFiltersDto,
HrPaginationDto,
} from './hr-filters.dto';

View File

@ -0,0 +1,180 @@
import {
IsString,
IsUUID,
IsOptional,
IsDateString,
IsEmail,
IsEnum,
MaxLength,
MinLength,
} from 'class-validator';
import { EmployeeStatus } from '../entities/employee.entity';
/**
* DTO for updating an existing employee
* All fields are optional since it's a partial update
*/
export class UpdateEmployeeDto {
@IsOptional()
@IsString()
@MinLength(1)
@MaxLength(100)
firstName?: string;
@IsOptional()
@IsString()
@MinLength(1)
@MaxLength(100)
lastName?: string;
@IsOptional()
@IsUUID()
userId?: string | null;
@IsOptional()
@IsUUID()
partnerId?: string | null;
@IsOptional()
@IsUUID()
departmentId?: string | null;
@IsOptional()
@IsUUID()
jobPositionId?: string | null;
@IsOptional()
@IsUUID()
managerId?: string | null;
// Work contact
@IsOptional()
@IsEmail()
@MaxLength(255)
workEmail?: string | null;
@IsOptional()
@IsString()
@MaxLength(50)
workPhone?: string | null;
@IsOptional()
@IsString()
@MaxLength(50)
workMobile?: string | null;
@IsOptional()
@IsString()
@MaxLength(255)
workLocation?: string | null;
// Personal data
@IsOptional()
@IsEmail()
@MaxLength(255)
personalEmail?: string | null;
@IsOptional()
@IsString()
@MaxLength(50)
personalPhone?: string | null;
@IsOptional()
@IsDateString()
birthDate?: string | null;
@IsOptional()
@IsString()
@MaxLength(20)
gender?: string | null;
@IsOptional()
@IsString()
@MaxLength(20)
maritalStatus?: string | null;
// Identification
@IsOptional()
@IsString()
@MaxLength(50)
identificationType?: string | null;
@IsOptional()
@IsString()
@MaxLength(100)
identificationNumber?: string | null;
@IsOptional()
@IsString()
@MaxLength(50)
taxId?: string | null;
@IsOptional()
@IsString()
@MaxLength(50)
socialSecurityNumber?: string | null;
// Address
@IsOptional()
@IsString()
@MaxLength(255)
street?: string | null;
@IsOptional()
@IsString()
@MaxLength(100)
city?: string | null;
@IsOptional()
@IsString()
@MaxLength(100)
state?: string | null;
@IsOptional()
@IsString()
@MaxLength(20)
postalCode?: string | null;
@IsOptional()
@IsString()
@MaxLength(100)
country?: string | null;
// Employment
@IsOptional()
@IsDateString()
hireDate?: string;
@IsOptional()
@IsDateString()
terminationDate?: string | null;
@IsOptional()
@IsEnum(['active', 'inactive', 'on_leave', 'terminated'])
status?: EmployeeStatus;
// Emergency contact
@IsOptional()
@IsString()
@MaxLength(255)
emergencyContactName?: string | null;
@IsOptional()
@IsString()
@MaxLength(50)
emergencyContactPhone?: string | null;
@IsOptional()
@IsString()
@MaxLength(50)
emergencyContactRelationship?: string | null;
// Metadata
@IsOptional()
@IsString()
notes?: string | null;
@IsOptional()
@IsString()
avatarUrl?: string | null;
}

View File

@ -0,0 +1,168 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Employee } from './employee.entity';
import { Department } from './department.entity';
import { JobPosition } from './job-position.entity';
/**
* Contract Type Enum
*/
export type ContractType = 'permanent' | 'temporary' | 'contractor' | 'internship' | 'part_time';
/**
* Contract Status Enum
*/
export type ContractStatus = 'draft' | 'active' | 'expired' | 'terminated' | 'cancelled';
/**
* Wage Type for payment frequency
*/
export type WageType = 'hourly' | 'daily' | 'weekly' | 'biweekly' | 'monthly' | 'annual';
/**
* Contract Entity (schema: hr.contracts)
*
* Employment contracts with details about compensation, duration,
* and terms of employment. Tracks contract lifecycle from draft to termination.
*
* RLS Policy: tenant_id = current_setting('app.current_tenant_id')
*/
@Entity({ name: 'contracts', schema: 'hr' })
export class Contract {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({ name: 'company_id', type: 'uuid' })
companyId: string;
@Index()
@Column({ name: 'employee_id', type: 'uuid' })
employeeId: string;
@ManyToOne(() => Employee, (employee) => employee.contracts, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'employee_id' })
employee: Employee;
@Column({ name: 'name', type: 'varchar', length: 255 })
name: string;
@Column({ name: 'reference', type: 'varchar', length: 50, nullable: true })
reference: string | null;
@Index()
@Column({
name: 'contract_type',
type: 'enum',
enum: ['permanent', 'temporary', 'contractor', 'internship', 'part_time'],
enumName: 'hr_contract_type',
default: 'permanent',
})
contractType: ContractType;
@Index()
@Column({
name: 'status',
type: 'enum',
enum: ['draft', 'active', 'expired', 'terminated', 'cancelled'],
enumName: 'hr_contract_status',
default: 'draft',
})
status: ContractStatus;
@Index()
@Column({ name: 'date_start', type: 'date' })
dateStart: Date;
@Column({ name: 'date_end', type: 'date', nullable: true })
dateEnd: Date | null;
@Column({ name: 'job_position_id', type: 'uuid', nullable: true })
jobPositionId: string | null;
@ManyToOne(() => JobPosition, { onDelete: 'SET NULL' })
@JoinColumn({ name: 'job_position_id' })
jobPosition: JobPosition | null;
@Column({ name: 'department_id', type: 'uuid', nullable: true })
departmentId: string | null;
@ManyToOne(() => Department, { onDelete: 'SET NULL' })
@JoinColumn({ name: 'department_id' })
department: Department | null;
// Compensation
@Column({ name: 'wage', type: 'decimal', precision: 15, scale: 2 })
wage: number;
@Column({
name: 'wage_type',
type: 'varchar',
length: 20,
default: 'monthly',
})
wageType: WageType;
@Column({ name: 'currency', type: 'varchar', length: 3, default: 'MXN' })
currency: string;
// Schedule
@Column({
name: 'hours_per_week',
type: 'decimal',
precision: 5,
scale: 2,
nullable: true,
default: 48,
})
hoursPerWeek: number | null;
@Column({ name: 'schedule_type', type: 'varchar', length: 50, nullable: true })
scheduleType: string | null;
// Trial period
@Column({ name: 'trial_period_months', type: 'integer', nullable: true, default: 0 })
trialPeriodMonths: number | null;
@Column({ name: 'trial_date_end', type: 'date', nullable: true })
trialDateEnd: Date | null;
// Metadata
@Column({ name: 'notes', type: 'text', nullable: true })
notes: string | null;
@Column({ name: 'document_url', type: 'text', nullable: true })
documentUrl: string | null;
// Audit
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
@Column({ name: 'activated_at', type: 'timestamptz', nullable: true })
activatedAt: Date | null;
@Column({ name: 'terminated_at', type: 'timestamptz', nullable: true })
terminatedAt: Date | null;
@Column({ name: 'terminated_by', type: 'uuid', nullable: true })
terminatedBy: string | null;
@Column({ name: 'termination_reason', type: 'text', nullable: true })
terminationReason: string | null;
}

View File

@ -0,0 +1,80 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
OneToMany,
JoinColumn,
} from 'typeorm';
import { Employee } from './employee.entity';
/**
* Department Entity (schema: hr.departments)
*
* Organizational departments with self-referential hierarchy.
* Supports parent/child relationships for department structure.
*
* RLS Policy: tenant_id = current_setting('app.current_tenant_id')
*/
@Entity({ name: 'departments', schema: 'hr' })
@Index(['tenantId', 'companyId', 'code'], { unique: true })
export class Department {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'company_id', type: 'uuid' })
companyId: string;
@Column({ name: 'name', type: 'varchar', length: 255 })
name: string;
@Column({ name: 'code', type: 'varchar', length: 50, nullable: true })
code: string | null;
@Index()
@Column({ name: 'parent_id', type: 'uuid', nullable: true })
parentId: string | null;
@ManyToOne(() => Department, (department) => department.children, {
onDelete: 'SET NULL',
})
@JoinColumn({ name: 'parent_id' })
parent: Department | null;
@OneToMany(() => Department, (department) => department.parent)
children: Department[];
@Column({ name: 'manager_id', type: 'uuid', nullable: true })
managerId: string | null;
@ManyToOne(() => Employee, { onDelete: 'SET NULL' })
@JoinColumn({ name: 'manager_id' })
manager: Employee | null;
@OneToMany(() => Employee, (employee) => employee.department)
employees: Employee[];
@Index()
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@Column({ name: 'description', type: 'text', nullable: true })
description: string | null;
@Column({ name: 'color', type: 'varchar', length: 7, nullable: true })
color: string | null;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,223 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
OneToMany,
JoinColumn,
} from 'typeorm';
import { Department } from './department.entity';
import { JobPosition } from './job-position.entity';
import { Contract } from './contract.entity';
import { Leave } from './leave.entity';
import { LeaveAllocation } from './leave-allocation.entity';
/**
* Employee Status Enum
*/
export type EmployeeStatus = 'active' | 'inactive' | 'on_leave' | 'terminated';
/**
* Employee Entity (schema: hr.employees)
*
* Core employee information including personal data, organization assignment,
* and employment details. Supports self-referential manager relationship.
*
* Relations:
* - user_id: References auth.users (optional - employee may not have system access)
* - partner_id: References partners.partners (optional - employee as business contact)
* - department_id: Current department assignment
* - job_position_id: Current job position
* - manager_id: Direct manager (self-reference)
*
* RLS Policy: tenant_id = current_setting('app.current_tenant_id')
*/
@Entity({ name: 'employees', schema: 'hr' })
@Index(['tenantId', 'employeeNumber'], { unique: true })
export class Employee {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'company_id', type: 'uuid' })
companyId: string;
@Column({ name: 'employee_number', type: 'varchar', length: 50 })
employeeNumber: string;
@Column({ name: 'first_name', type: 'varchar', length: 100 })
firstName: string;
@Column({ name: 'last_name', type: 'varchar', length: 100 })
lastName: string;
/**
* Generated column in PostgreSQL: first_name || ' ' || last_name
* Mark as insert: false, update: false since it's computed by DB
*/
@Index()
@Column({
name: 'full_name',
type: 'varchar',
length: 255,
insert: false,
update: false,
nullable: true,
})
fullName: string;
@Index()
@Column({ name: 'user_id', type: 'uuid', nullable: true })
userId: string | null;
@Column({ name: 'partner_id', type: 'uuid', nullable: true })
partnerId: string | null;
@Index()
@Column({ name: 'department_id', type: 'uuid', nullable: true })
departmentId: string | null;
@ManyToOne(() => Department, (department) => department.employees, {
onDelete: 'SET NULL',
})
@JoinColumn({ name: 'department_id' })
department: Department | null;
@Index()
@Column({ name: 'job_position_id', type: 'uuid', nullable: true })
jobPositionId: string | null;
@ManyToOne(() => JobPosition, (position) => position.employees, {
onDelete: 'SET NULL',
})
@JoinColumn({ name: 'job_position_id' })
jobPosition: JobPosition | null;
@Index()
@Column({ name: 'manager_id', type: 'uuid', nullable: true })
managerId: string | null;
@ManyToOne(() => Employee, (employee) => employee.subordinates, {
onDelete: 'SET NULL',
})
@JoinColumn({ name: 'manager_id' })
manager: Employee | null;
@OneToMany(() => Employee, (employee) => employee.manager)
subordinates: Employee[];
// Work contact
@Column({ name: 'work_email', type: 'varchar', length: 255, nullable: true })
workEmail: string | null;
@Column({ name: 'work_phone', type: 'varchar', length: 50, nullable: true })
workPhone: string | null;
@Column({ name: 'work_mobile', type: 'varchar', length: 50, nullable: true })
workMobile: string | null;
@Column({ name: 'work_location', type: 'varchar', length: 255, nullable: true })
workLocation: string | null;
// Personal data
@Column({ name: 'personal_email', type: 'varchar', length: 255, nullable: true })
personalEmail: string | null;
@Column({ name: 'personal_phone', type: 'varchar', length: 50, nullable: true })
personalPhone: string | null;
@Column({ name: 'birth_date', type: 'date', nullable: true })
birthDate: Date | null;
@Column({ name: 'gender', type: 'varchar', length: 20, nullable: true })
gender: string | null;
@Column({ name: 'marital_status', type: 'varchar', length: 20, nullable: true })
maritalStatus: string | null;
// Official identification
@Column({ name: 'identification_type', type: 'varchar', length: 50, nullable: true })
identificationType: string | null;
@Column({ name: 'identification_number', type: 'varchar', length: 100, nullable: true })
identificationNumber: string | null;
@Column({ name: 'tax_id', type: 'varchar', length: 50, nullable: true })
taxId: string | null;
@Column({ name: 'social_security_number', type: 'varchar', length: 50, nullable: true })
socialSecurityNumber: string | null;
// Address
@Column({ name: 'street', type: 'varchar', length: 255, nullable: true })
street: string | null;
@Column({ name: 'city', type: 'varchar', length: 100, nullable: true })
city: string | null;
@Column({ name: 'state', type: 'varchar', length: 100, nullable: true })
state: string | null;
@Column({ name: 'postal_code', type: 'varchar', length: 20, nullable: true })
postalCode: string | null;
@Column({ name: 'country', type: 'varchar', length: 100, nullable: true })
country: string | null;
// Employment
@Column({ name: 'hire_date', type: 'date' })
hireDate: Date;
@Column({ name: 'termination_date', type: 'date', nullable: true })
terminationDate: Date | null;
@Index()
@Column({
name: 'status',
type: 'enum',
enum: ['active', 'inactive', 'on_leave', 'terminated'],
enumName: 'hr_employee_status',
default: 'active',
})
status: EmployeeStatus;
// Emergency contact
@Column({ name: 'emergency_contact_name', type: 'varchar', length: 255, nullable: true })
emergencyContactName: string | null;
@Column({ name: 'emergency_contact_phone', type: 'varchar', length: 50, nullable: true })
emergencyContactPhone: string | null;
@Column({ name: 'emergency_contact_relationship', type: 'varchar', length: 50, nullable: true })
emergencyContactRelationship: string | null;
// Metadata
@Column({ name: 'notes', type: 'text', nullable: true })
notes: string | null;
@Column({ name: 'avatar_url', type: 'text', nullable: true })
avatarUrl: string | null;
// Relations
@OneToMany(() => Contract, (contract) => contract.employee)
contracts: Contract[];
@OneToMany(() => Leave, (leave) => leave.employee)
leaves: Leave[];
@OneToMany(() => LeaveAllocation, (allocation) => allocation.employee)
leaveAllocations: LeaveAllocation[];
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,7 @@
export { Department } from './department.entity';
export { JobPosition } from './job-position.entity';
export { Employee, EmployeeStatus } from './employee.entity';
export { Contract, ContractType, ContractStatus, WageType } from './contract.entity';
export { LeaveType, LeaveTypeCategory, AllocationType } from './leave-type.entity';
export { LeaveAllocation } from './leave-allocation.entity';
export { Leave, LeaveStatus, HalfDayType } from './leave.entity';

View File

@ -0,0 +1,65 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
OneToMany,
JoinColumn,
} from 'typeorm';
import { Department } from './department.entity';
import { Employee } from './employee.entity';
/**
* Job Position Entity (schema: hr.job_positions)
*
* Defines job titles/positions within the organization.
* Can be associated with a specific department.
*
* RLS Policy: tenant_id = current_setting('app.current_tenant_id')
*/
@Entity({ name: 'job_positions', schema: 'hr' })
@Index(['tenantId', 'companyId', 'code'], { unique: true })
export class JobPosition {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({ name: 'company_id', type: 'uuid' })
companyId: string;
@Column({ name: 'name', type: 'varchar', length: 255 })
name: string;
@Column({ name: 'code', type: 'varchar', length: 50, nullable: true })
code: string | null;
@Column({ name: 'description', type: 'text', nullable: true })
description: string | null;
@Index()
@Column({ name: 'department_id', type: 'uuid', nullable: true })
departmentId: string | null;
@ManyToOne(() => Department, { onDelete: 'SET NULL' })
@JoinColumn({ name: 'department_id' })
department: Department | null;
@OneToMany(() => Employee, (employee) => employee.jobPosition)
employees: Employee[];
@Index()
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,89 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Employee } from './employee.entity';
import { LeaveType } from './leave-type.entity';
/**
* Leave Allocation Entity (schema: hr.leave_allocations)
*
* Tracks allocated leave days per employee and leave type.
* Supports period-based allocations with used/remaining tracking.
*
* Note: days_remaining is a computed column in PostgreSQL
*
* RLS Policy: tenant_id = current_setting('app.current_tenant_id')
*/
@Entity({ name: 'leave_allocations', schema: 'hr' })
export class LeaveAllocation {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'employee_id', type: 'uuid' })
employeeId: string;
@ManyToOne(() => Employee, (employee) => employee.leaveAllocations, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'employee_id' })
employee: Employee;
@Index()
@Column({ name: 'leave_type_id', type: 'uuid' })
leaveTypeId: string;
@ManyToOne(() => LeaveType, (leaveType) => leaveType.allocations, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'leave_type_id' })
leaveType: LeaveType;
@Column({ name: 'days_allocated', type: 'decimal', precision: 5, scale: 2 })
daysAllocated: number;
@Column({ name: 'days_used', type: 'decimal', precision: 5, scale: 2, default: 0 })
daysUsed: number;
/**
* Generated column in PostgreSQL: days_allocated - days_used
* Mark as insert: false, update: false since it's computed by DB
*/
@Column({
name: 'days_remaining',
type: 'decimal',
precision: 5,
scale: 2,
insert: false,
update: false,
nullable: true,
})
daysRemaining: number;
@Index()
@Column({ name: 'date_from', type: 'date' })
dateFrom: Date;
@Column({ name: 'date_to', type: 'date' })
dateTo: Date;
@Column({ name: 'notes', type: 'text', nullable: true })
notes: string | null;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,131 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
OneToMany,
} from 'typeorm';
import { Leave } from './leave.entity';
import { LeaveAllocation } from './leave-allocation.entity';
/**
* Leave Type Category Enum
*/
export type LeaveTypeCategory =
| 'vacation'
| 'sick'
| 'personal'
| 'maternity'
| 'paternity'
| 'bereavement'
| 'unpaid'
| 'other';
/**
* Allocation Type Enum
*/
export type AllocationType = 'fixed' | 'accrual' | 'unlimited';
/**
* Leave Type Entity (schema: hr.leave_types)
*
* Configurable leave/absence types for the organization.
* Defines rules for approval, allocation, and payment.
*
* Examples: Vacation, Sick Leave, Maternity, etc.
*
* RLS Policy: tenant_id = current_setting('app.current_tenant_id')
*/
@Entity({ name: 'leave_types', schema: 'hr' })
@Index(['tenantId', 'companyId', 'code'], { unique: true })
export class LeaveType {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'company_id', type: 'uuid' })
companyId: string;
@Column({ name: 'name', type: 'varchar', length: 255 })
name: string;
@Column({ name: 'code', type: 'varchar', length: 50 })
code: string;
@Column({ name: 'description', type: 'text', nullable: true })
description: string | null;
@Column({ name: 'color', type: 'varchar', length: 7, default: '#3B82F6' })
color: string;
@Index()
@Column({
name: 'leave_category',
type: 'enum',
enum: ['vacation', 'sick', 'personal', 'maternity', 'paternity', 'bereavement', 'unpaid', 'other'],
enumName: 'hr_leave_type_category',
default: 'other',
})
leaveCategory: LeaveTypeCategory;
@Column({
name: 'allocation_type',
type: 'enum',
enum: ['fixed', 'accrual', 'unlimited'],
enumName: 'hr_allocation_type',
default: 'fixed',
})
allocationType: AllocationType;
@Column({ name: 'requires_approval', type: 'boolean', default: true })
requiresApproval: boolean;
@Column({ name: 'requires_document', type: 'boolean', default: false })
requiresDocument: boolean;
// Limits
@Column({ name: 'max_days_per_request', type: 'integer', nullable: true })
maxDaysPerRequest: number | null;
@Column({ name: 'max_days_per_year', type: 'integer', nullable: true })
maxDaysPerYear: number | null;
@Column({ name: 'min_days_notice', type: 'integer', default: 0 })
minDaysNotice: number;
// Payment
@Column({ name: 'is_paid', type: 'boolean', default: true })
isPaid: boolean;
@Column({
name: 'pay_percentage',
type: 'decimal',
precision: 5,
scale: 2,
default: 100,
})
payPercentage: number;
@Index()
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
// Relations
@OneToMany(() => Leave, (leave) => leave.leaveType)
leaves: Leave[];
@OneToMany(() => LeaveAllocation, (allocation) => allocation.leaveType)
allocations: LeaveAllocation[];
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,142 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Employee } from './employee.entity';
import { LeaveType } from './leave-type.entity';
import { LeaveAllocation } from './leave-allocation.entity';
/**
* Leave Status Enum
*/
export type LeaveStatus = 'draft' | 'submitted' | 'approved' | 'rejected' | 'cancelled';
/**
* Half Day Type
*/
export type HalfDayType = 'morning' | 'afternoon';
/**
* Leave Entity (schema: hr.leaves)
*
* Leave/absence requests from employees.
* Tracks the full lifecycle from draft to approval/rejection.
*
* RLS Policy: tenant_id = current_setting('app.current_tenant_id')
*/
@Entity({ name: 'leaves', schema: 'hr' })
export class Leave {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'company_id', type: 'uuid' })
companyId: string;
@Index()
@Column({ name: 'employee_id', type: 'uuid' })
employeeId: string;
@ManyToOne(() => Employee, (employee) => employee.leaves, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'employee_id' })
employee: Employee;
@Index()
@Column({ name: 'leave_type_id', type: 'uuid' })
leaveTypeId: string;
@ManyToOne(() => LeaveType, (leaveType) => leaveType.leaves, {
onDelete: 'RESTRICT',
})
@JoinColumn({ name: 'leave_type_id' })
leaveType: LeaveType;
@Column({ name: 'allocation_id', type: 'uuid', nullable: true })
allocationId: string | null;
@ManyToOne(() => LeaveAllocation, { onDelete: 'SET NULL' })
@JoinColumn({ name: 'allocation_id' })
allocation: LeaveAllocation | null;
// Period
@Index()
@Column({ name: 'date_from', type: 'date' })
dateFrom: Date;
@Column({ name: 'date_to', type: 'date' })
dateTo: Date;
@Column({ name: 'days_requested', type: 'decimal', precision: 5, scale: 2 })
daysRequested: number;
// Half day support
@Column({ name: 'is_half_day', type: 'boolean', default: false })
isHalfDay: boolean;
@Column({
name: 'half_day_type',
type: 'varchar',
length: 20,
nullable: true,
})
halfDayType: HalfDayType | null;
@Index()
@Column({
name: 'status',
type: 'enum',
enum: ['draft', 'submitted', 'approved', 'rejected', 'cancelled'],
enumName: 'hr_leave_status',
default: 'draft',
})
status: LeaveStatus;
// Approval
@Index()
@Column({ name: 'approver_id', type: 'uuid', nullable: true })
approverId: string | null;
@Column({ name: 'approved_at', type: 'timestamptz', nullable: true })
approvedAt: Date | null;
@Column({ name: 'rejection_reason', type: 'text', nullable: true })
rejectionReason: string | null;
// Metadata
@Column({ name: 'request_reason', type: 'text', nullable: true })
requestReason: string | null;
@Column({ name: 'document_url', type: 'text', nullable: true })
documentUrl: string | null;
@Column({ name: 'notes', type: 'text', nullable: true })
notes: string | null;
// Audit
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
@Column({ name: 'submitted_at', type: 'timestamptz', nullable: true })
submittedAt: Date | null;
@Column({ name: 'cancelled_at', type: 'timestamptz', nullable: true })
cancelledAt: Date | null;
@Column({ name: 'cancelled_by', type: 'uuid', nullable: true })
cancelledBy: string | null;
}