Commit ec80f55d authored by Administrator's avatar Administrator

Update 22 files via Son of Anton

parent 0b0931ce
...@@ -63,6 +63,12 @@ import { ApiKeysModule } from './modules/api-keys/api-keys.module'; ...@@ -63,6 +63,12 @@ import { ApiKeysModule } from './modules/api-keys/api-keys.module';
import { WebhooksModule } from './modules/webhooks/webhooks.module'; import { WebhooksModule } from './modules/webhooks/webhooks.module';
import { SearchModule } from './modules/search/search.module'; import { SearchModule } from './modules/search/search.module';
// ─── Phase 3C: Documents & Offboarding ──────────────────────
import { ContractsModule } from './modules/contracts/contracts.module';
import { PoliciesModule } from './modules/policies/policies.module';
import { OffboardingModule } from './modules/offboarding/offboarding.module';
import { PdfModule } from './modules/pdf/pdf.module';
import { JwtAuthGuard } from './common/guards/jwt-auth.guard'; import { JwtAuthGuard } from './common/guards/jwt-auth.guard';
import { RolesGuard } from './common/guards/roles.guard'; import { RolesGuard } from './common/guards/roles.guard';
import { TransformInterceptor } from './common/interceptors/transform.interceptor'; import { TransformInterceptor } from './common/interceptors/transform.interceptor';
...@@ -122,6 +128,11 @@ import { RateLimitMiddleware } from './common/middleware/rate-limit.middleware'; ...@@ -122,6 +128,11 @@ import { RateLimitMiddleware } from './common/middleware/rate-limit.middleware';
ApiKeysModule, ApiKeysModule,
WebhooksModule, WebhooksModule,
SearchModule, SearchModule,
// Phase 3C
ContractsModule,
PoliciesModule,
OffboardingModule,
PdfModule,
], ],
providers: [ providers: [
{ provide: APP_GUARD, useClass: JwtAuthGuard }, { provide: APP_GUARD, useClass: JwtAuthGuard },
......
import {
Controller,
Get,
Put,
Delete,
Body,
Param,
Query,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { ContractsService } from './contracts.service';
import { ContractFilterDto } from './dto/contract-filter.dto';
import { UpdateContractMetadataDto } from './dto/update-contract.dto';
import { CurrentUser, RequestUser } from '../../common/decorators/current-user.decorator';
import { Roles } from '../../common/decorators/roles.decorator';
@Controller('contracts')
export class ContractsController {
constructor(private readonly contractsService: ContractsService) {}
@Get()
async findAll(@Query() filter: ContractFilterDto, @CurrentUser() user: RequestUser) {
return this.contractsService.findAll(filter, user);
}
@Get('expiring')
@Roles('SUPER_ADMIN', 'ADMIN')
async getExpiring(@Query('days') days: string, @CurrentUser() user: RequestUser) {
const daysOut = days ? parseInt(days, 10) : 90;
return this.contractsService.getExpiringContracts(daysOut, user);
}
@Get('user/:userId')
async findByUser(@Param('userId') userId: string, @CurrentUser() user: RequestUser) {
return this.contractsService.findByUserId(userId, user);
}
@Get(':id')
async findById(@Param('id') id: string, @CurrentUser() user: RequestUser) {
return this.contractsService.findById(id, user);
}
@Get(':id/snapshot')
async getSnapshot(@Param('id') id: string, @CurrentUser() user: RequestUser) {
return this.contractsService.getContractSnapshot(id, user);
}
@Put(':id')
@Roles('SUPER_ADMIN')
async updateMetadata(
@Param('id') id: string,
@Body() dto: UpdateContractMetadataDto,
@CurrentUser() user: RequestUser,
) {
return this.contractsService.updateMetadata(id, dto, user);
}
@Delete(':id')
@Roles('SUPER_ADMIN')
@HttpCode(HttpStatus.OK)
async delete(@Param('id') id: string, @CurrentUser() user: RequestUser) {
await this.contractsService.delete(id, user);
return { message: 'Contract record deleted' };
}
}
\ No newline at end of file
import { Module } from '@nestjs/common';
import { ContractsController } from './contracts.controller';
import { ContractsService } from './contracts.service';
@Module({
controllers: [ContractsController],
providers: [ContractsService],
exports: [ContractsService],
})
export class ContractsModule {}
\ No newline at end of file
import {
Injectable,
NotFoundException,
ForbiddenException,
BadRequestException,
Logger,
} from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { ContractFilterDto } from './dto/contract-filter.dto';
import { UpdateContractMetadataDto } from './dto/update-contract.dto';
import { RequestUser } from '../../common/decorators/current-user.decorator';
import { getSkip, buildPaginatedResponse, PaginatedResult } from '../../common/utils/pagination.util';
@Injectable()
export class ContractsService {
private readonly logger = new Logger(ContractsService.name);
constructor(private readonly prisma: PrismaService) {}
async findAll(filter: ContractFilterDto, currentUser: RequestUser): Promise<PaginatedResult<any>> {
const page = filter.page || 1;
const limit = filter.limit || 20;
const where: any = {};
if (currentUser.role === 'CONTRACTOR') {
where.userId = currentUser.id;
}
if (currentUser.role === 'TEAM_LEAD') {
throw new ForbiddenException('Project Leaders cannot access contract records');
}
if (filter.userId) where.userId = filter.userId;
if (filter.status) where.status = filter.status;
if (filter.contractType) where.contractType = filter.contractType;
if (filter.expiringBefore || filter.expiringAfter) {
where.endDate = {};
if (filter.expiringBefore) where.endDate.lte = new Date(filter.expiringBefore);
if (filter.expiringAfter) where.endDate.gte = new Date(filter.expiringAfter);
}
const [data, total] = await Promise.all([
this.prisma.contract.findMany({
where,
skip: getSkip(page, limit),
take: limit,
orderBy: { createdAt: filter.sortOrder || 'desc' },
include: {
user: { select: { id: true, firstName: true, lastName: true, avatar: true, contractorType: true } },
},
}),
this.prisma.contract.count({ where }),
]);
const sanitized = data.map((c: any) => {
if (currentUser.role === 'CONTRACTOR') {
const { notes, ...rest } = c;
return rest;
}
return c;
});
return buildPaginatedResponse(sanitized, total, { page, limit, sortOrder: filter.sortOrder || 'desc' });
}
async findById(id: string, currentUser: RequestUser): Promise<any> {
const contract = await this.prisma.contract.findUnique({
where: { id },
include: {
user: { select: { id: true, firstName: true, lastName: true, avatar: true, contractorType: true, email: true } },
},
});
if (!contract) throw new NotFoundException('Contract not found');
if (currentUser.role === 'CONTRACTOR' && contract.userId !== currentUser.id) {
throw new ForbiddenException('You can only view your own contracts');
}
if (currentUser.role === 'TEAM_LEAD') {
throw new ForbiddenException('Project Leaders cannot access contract records');
}
return contract;
}
async findByUserId(userId: string, currentUser: RequestUser): Promise<any[]> {
if (currentUser.role === 'CONTRACTOR' && userId !== currentUser.id) {
throw new ForbiddenException('You can only view your own contracts');
}
if (currentUser.role === 'TEAM_LEAD') {
throw new ForbiddenException('Project Leaders cannot access contract records');
}
return this.prisma.contract.findMany({
where: { userId },
orderBy: { createdAt: 'desc' },
include: {
user: { select: { id: true, firstName: true, lastName: true } },
},
});
}
async updateMetadata(id: string, dto: UpdateContractMetadataDto, currentUser: RequestUser): Promise<any> {
if (currentUser.role !== 'SUPER_ADMIN') {
throw new ForbiddenException('Only Super Admin can edit contract metadata');
}
const contract = await this.prisma.contract.findUnique({ where: { id } });
if (!contract) throw new NotFoundException('Contract not found');
const updateData: any = {};
if (dto.contractType !== undefined) updateData.contractType = dto.contractType;
if (dto.startDate !== undefined) updateData.startDate = new Date(dto.startDate);
if (dto.endDate !== undefined) updateData.endDate = dto.endDate ? new Date(dto.endDate) : null;
if (dto.notes !== undefined) updateData.notes = dto.notes;
if (dto.status !== undefined) updateData.status = dto.status;
const updated = await this.prisma.contract.update({
where: { id },
data: updateData,
include: {
user: { select: { id: true, firstName: true, lastName: true } },
},
});
this.logger.log(`Contract ${id} metadata updated by ${currentUser.email}`);
return updated;
}
async delete(id: string, currentUser: RequestUser): Promise<void> {
if (currentUser.role !== 'SUPER_ADMIN') {
throw new ForbiddenException('Only Super Admin can delete contract records');
}
const contract = await this.prisma.contract.findUnique({ where: { id } });
if (!contract) throw new NotFoundException('Contract not found');
await this.prisma.contract.delete({ where: { id } });
this.logger.log(`Contract ${id} deleted by ${currentUser.email}`);
}
async getExpiringContracts(daysOut: number, currentUser: RequestUser): Promise<any[]> {
if (currentUser.role !== 'SUPER_ADMIN' && currentUser.role !== 'ADMIN') {
throw new ForbiddenException('Only Super Admin and Admin can view expiring contracts');
}
const targetDate = new Date();
targetDate.setDate(targetDate.getDate() + daysOut);
return this.prisma.contract.findMany({
where: {
endDate: { lte: targetDate, gte: new Date() },
status: 'ACTIVE',
},
include: {
user: { select: { id: true, firstName: true, lastName: true, avatar: true, status: true } },
},
orderBy: { endDate: 'asc' },
});
}
async getContractSnapshot(id: string, currentUser: RequestUser): Promise<{ html: string; signatureData: any }> {
const contract = await this.prisma.contract.findUnique({
where: { id },
select: { userId: true, snapshotHtml: true, signatureData: true },
});
if (!contract) throw new NotFoundException('Contract not found');
if (currentUser.role === 'CONTRACTOR' && contract.userId !== currentUser.id) {
throw new ForbiddenException('You can only view your own contracts');
}
if (currentUser.role === 'TEAM_LEAD') {
throw new ForbiddenException('Project Leaders cannot access contracts');
}
return {
html: contract.snapshotHtml || '',
signatureData: contract.signatureData || null,
};
}
}
\ No newline at end of file
import { IsOptional, IsString, IsDateString } from 'class-validator';
import { PaginationDto } from '../../../common/dto/pagination.dto';
export class ContractFilterDto extends PaginationDto {
@IsOptional()
@IsString()
userId?: string;
@IsOptional()
@IsString()
status?: string;
@IsOptional()
@IsString()
contractType?: string;
@IsOptional()
@IsDateString()
expiringBefore?: string;
@IsOptional()
@IsDateString()
expiringAfter?: string;
}
\ No newline at end of file
export class ContractResponseDto {
id: string;
userId: string;
contractType: string;
status: string;
startDate: string | null;
endDate: string | null;
signedAt: string | null;
snapshotHtml: string | null;
baseSalaryAtSigning: number | null;
actualSalaryAtSigning: number | null;
scheduleAtSigning: any;
signatureData: any;
notes: string | null;
user: {
id: string;
firstName: string;
lastName: string;
};
createdAt: string;
updatedAt: string;
}
\ No newline at end of file
import { IsOptional, IsString, IsDateString } from 'class-validator';
export class UpdateContractMetadataDto {
@IsOptional()
@IsString()
contractType?: string;
@IsOptional()
@IsDateString()
startDate?: string;
@IsOptional()
@IsDateString()
endDate?: string;
@IsOptional()
@IsString()
notes?: string;
@IsOptional()
@IsString()
status?: string;
}
\ No newline at end of file
import { IsString, IsOptional, IsDateString, MinLength } from 'class-validator';
export class CreateTerminationDto {
@IsString()
userId: string;
@IsString()
terminationType: string; // VOLUNTARY, FOR_CAUSE, MUTUAL_AGREEMENT, CONTRACT_EXPIRY
@IsString()
@MinLength(100, { message: 'Termination reason must be at least 100 characters' })
reason: string;
@IsDateString()
effectiveDate: string;
@IsOptional()
@IsString()
notes?: string;
}
\ No newline at end of file
import { IsOptional, IsString, IsDateString } from 'class-validator';
import { PaginationDto } from '../../../common/dto/pagination.dto';
export class TerminationFilterDto extends PaginationDto {
@IsOptional()
@IsString()
userId?: string;
@IsOptional()
@IsString()
terminationType?: string;
@IsOptional()
@IsString()
status?: string;
@IsOptional()
@IsDateString()
dateFrom?: string;
@IsOptional()
@IsDateString()
dateTo?: string;
}
\ No newline at end of file
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { OffboardingService } from './offboarding.service';
import { CreateTerminationDto } from './dto/create-termination.dto';
import { TerminationFilterDto } from './dto/termination-filter.dto';
import { CurrentUser, RequestUser } from '../../common/decorators/current-user.decorator';
import { Roles } from '../../common/decorators/roles.decorator';
@Controller('offboarding')
export class OffboardingController {
constructor(private readonly offboardingService: OffboardingService) {}
@Post()
@Roles('SUPER_ADMIN', 'ADMIN')
async initiate(@Body() dto: CreateTerminationDto, @CurrentUser() user: RequestUser) {
return this.offboardingService.initiate(dto, user);
}
@Get()
@Roles('SUPER_ADMIN', 'ADMIN')
async findAll(@Query() filter: TerminationFilterDto, @CurrentUser() user: RequestUser) {
return this.offboardingService.findAll(filter, user);
}
@Get(':id')
async findById(@Param('id') id: string, @CurrentUser() user: RequestUser) {
return this.offboardingService.findById(id, user);
}
@Put(':id/checklist/:itemId')
@Roles('SUPER_ADMIN', 'ADMIN')
@HttpCode(HttpStatus.OK)
async updateChecklist(
@Param('id') id: string,
@Param('itemId') itemId: string,
@Body('completed') completed: boolean,
@CurrentUser() user: RequestUser,
) {
return this.offboardingService.updateChecklist(id, itemId, completed, user);
}
@Post(':id/complete')
@Roles('SUPER_ADMIN')
@HttpCode(HttpStatus.OK)
async complete(@Param('id') id: string, @CurrentUser() user: RequestUser) {
return this.offboardingService.complete(id, user);
}
@Put(':id')
@Roles('SUPER_ADMIN')
async update(@Param('id') id: string, @Body() data: any, @CurrentUser() user: RequestUser) {
return this.offboardingService.update(id, data, user);
}
@Delete(':id')
@Roles('SUPER_ADMIN')
@HttpCode(HttpStatus.OK)
async delete(@Param('id') id: string, @CurrentUser() user: RequestUser) {
await this.offboardingService.delete(id, user);
return { message: 'Termination record deleted' };
}
}
\ No newline at end of file
import { Module } from '@nestjs/common';
import { OffboardingController } from './offboarding.controller';
import { OffboardingService } from './offboarding.service';
import { NotificationsModule } from '../notifications/notifications.module';
@Module({
imports: [NotificationsModule],
controllers: [OffboardingController],
providers: [OffboardingService],
exports: [OffboardingService],
})
export class OffboardingModule {}
\ No newline at end of file
This diff is collapsed.
import {
Controller,
Get,
Param,
Query,
Res,
} from '@nestjs/common';
import { Response } from 'express';
import { PdfService } from './pdf.service';
import { CurrentUser, RequestUser } from '../../common/decorators/current-user.decorator';
import { Roles } from '../../common/decorators/roles.decorator';
@Controller('pdf')
export class PdfController {
constructor(private readonly pdfService: PdfService) {}
@Get('payslip/:userId')
async getPayslipData(
@Param('userId') userId: string,
@Query('month') month: string,
@Query('year') year: string,
@CurrentUser() user: RequestUser,
) {
const m = month ? parseInt(month, 10) : new Date().getMonth() + 1;
const y = year ? parseInt(year, 10) : new Date().getFullYear();
return this.pdfService.generatePayslipData(userId, m, y, user);
}
@Get('payslip/my')
async getMyPayslipData(
@Query('month') month: string,
@Query('year') year: string,
@CurrentUser() user: RequestUser,
) {
const m = month ? parseInt(month, 10) : new Date().getMonth() + 1;
const y = year ? parseInt(year, 10) : new Date().getFullYear();
return this.pdfService.generatePayslipData(user.id, m, y, user);
}
@Get('evaluation/:evaluationId')
async getEvaluationData(
@Param('evaluationId') evaluationId: string,
@CurrentUser() user: RequestUser,
) {
return this.pdfService.generateEvaluationData(evaluationId, user);
}
@Get('payroll-summary')
@Roles('SUPER_ADMIN', 'ADMIN')
async getPayrollSummaryData(
@Query('month') month: string,
@Query('year') year: string,
@CurrentUser() user: RequestUser,
) {
const m = month ? parseInt(month, 10) : new Date().getMonth() + 1;
const y = year ? parseInt(year, 10) : new Date().getFullYear();
return this.pdfService.generatePayrollSummaryData(m, y, user);
}
}
\ No newline at end of file
import { Module } from '@nestjs/common';
import { PdfController } from './pdf.controller';
import { PdfService } from './pdf.service';
@Module({
controllers: [PdfController],
providers: [PdfService],
exports: [PdfService],
})
export class PdfModule {}
\ No newline at end of file
This diff is collapsed.
import { IsString, IsOptional, IsBoolean, MinLength, MaxLength } from 'class-validator';
export class CreatePolicyDto {
@IsString()
@MinLength(2)
@MaxLength(200)
name: string;
@IsString()
@MinLength(50)
content: string;
@IsOptional()
@IsBoolean()
requiresAcknowledgment?: boolean;
@IsOptional()
@IsString()
category?: string;
}
\ No newline at end of file
import { IsOptional, IsString, IsBoolean } from 'class-validator';
import { Type } from 'class-transformer';
import { PaginationDto } from '../../../common/dto/pagination.dto';
export class PolicyFilterDto extends PaginationDto {
@IsOptional()
@IsString()
category?: string;
@IsOptional()
@Type(() => Boolean)
@IsBoolean()
isActive?: boolean;
@IsOptional()
@Type(() => Boolean)
@IsBoolean()
requiresAcknowledgment?: boolean;
}
\ No newline at end of file
import { IsString, IsOptional, IsBoolean, MinLength, MaxLength } from 'class-validator';
export class UpdatePolicyDto {
@IsOptional()
@IsString()
@MinLength(2)
@MaxLength(200)
name?: string;
@IsOptional()
@IsString()
@MinLength(50)
content?: string;
@IsOptional()
@IsBoolean()
requiresAcknowledgment?: boolean;
@IsOptional()
@IsBoolean()
isActive?: boolean;
@IsOptional()
@IsString()
category?: string;
}
\ No newline at end of file
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { PoliciesService } from './policies.service';
import { CreatePolicyDto } from './dto/create-policy.dto';
import { UpdatePolicyDto } from './dto/update-policy.dto';
import { PolicyFilterDto } from './dto/policy-filter.dto';
import { CurrentUser, RequestUser } from '../../common/decorators/current-user.decorator';
import { Roles } from '../../common/decorators/roles.decorator';
@Controller('policies')
export class PoliciesController {
constructor(private readonly policiesService: PoliciesService) {}
@Post()
@Roles('SUPER_ADMIN')
async create(@Body() dto: CreatePolicyDto, @CurrentUser() user: RequestUser) {
return this.policiesService.create(dto, user);
}
@Get()
async findAll(@Query() filter: PolicyFilterDto, @CurrentUser() user: RequestUser) {
return this.policiesService.findAll(filter, user);
}
@Get('unacknowledged')
async getUnacknowledged(@CurrentUser() user: RequestUser) {
return this.policiesService.getUnacknowledgedPolicies(user.id);
}
@Get(':id')
async findById(@Param('id') id: string, @CurrentUser() user: RequestUser) {
return this.policiesService.findById(id, user);
}
@Get(':id/versions')
@Roles('SUPER_ADMIN')
async getVersions(@Param('id') id: string, @CurrentUser() user: RequestUser) {
return this.policiesService.getPreviousVersions(id, user);
}
@Get(':id/acknowledgments')
@Roles('SUPER_ADMIN', 'ADMIN')
async getAcknowledgments(@Param('id') id: string, @CurrentUser() user: RequestUser) {
return this.policiesService.getAcknowledgmentStatus(id, user);
}
@Post(':id/acknowledge')
@HttpCode(HttpStatus.OK)
async acknowledge(@Param('id') id: string, @CurrentUser() user: RequestUser) {
return this.policiesService.acknowledge(id, user);
}
@Put(':id')
@Roles('SUPER_ADMIN')
async update(@Param('id') id: string, @Body() dto: UpdatePolicyDto, @CurrentUser() user: RequestUser) {
return this.policiesService.update(id, dto, user);
}
@Post(':id/archive')
@Roles('SUPER_ADMIN')
@HttpCode(HttpStatus.OK)
async archive(@Param('id') id: string, @CurrentUser() user: RequestUser) {
return this.policiesService.archive(id, user);
}
@Delete(':id')
@Roles('SUPER_ADMIN')
@HttpCode(HttpStatus.OK)
async delete(@Param('id') id: string, @CurrentUser() user: RequestUser) {
await this.policiesService.delete(id, user);
return { message: 'Policy deleted' };
}
}
\ No newline at end of file
import { Module } from '@nestjs/common';
import { PoliciesController } from './policies.controller';
import { PoliciesService } from './policies.service';
import { NotificationsModule } from '../notifications/notifications.module';
@Module({
imports: [NotificationsModule],
controllers: [PoliciesController],
providers: [PoliciesService],
exports: [PoliciesService],
})
export class PoliciesModule {}
\ No newline at end of file
This diff is collapsed.
// ─── OFFBOARDING & DOCUMENT MODELS ─────────────────────────
model Termination {
id String @id @default(uuid())
userId String
user User @relation("UserTerminations", fields: [userId], references: [id], onDelete: RESTRICT)
terminationType String // VOLUNTARY, FOR_CAUSE, MUTUAL_AGREEMENT, CONTRACT_EXPIRY
reason String
effectiveDate DateTime
noticeDate DateTime
notes String?
status String @default("INITIATED") // INITIATED, CHECKLIST_COMPLETE, COMPLETED, CANCELLED
initiatedById String
initiatedBy User @relation("TerminationInitiator", fields: [initiatedById], references: [id], onDelete: RESTRICT)
completedAt DateTime?
completedById String?
checklist OffboardingChecklistItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([status])
@@index([effectiveDate])
}
model OffboardingChecklistItem {
id String @id @default(uuid())
terminationId String
termination Termination @relation(fields: [terminationId], references: [id], onDelete: CASCADE)
title String
verifiedBy String // SYSTEM, SUPER_ADMIN, ADMIN, PROJECT_LEADER
position Int
isCompleted Boolean @default(false)
completedAt DateTime?
completedById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([terminationId])
}
model Policy {
id String @id @default(uuid())
name String
content String
version Int @default(1)
category String @default("GENERAL")
requiresAcknowledgment Boolean @default(true)
isActive Boolean @default(true)
publishedAt DateTime?
createdById String
createdBy User @relation("PolicyCreator", fields: [createdById], references: [id], onDelete: RESTRICT)
acknowledgments PolicyAcknowledgment[]
versions PolicyVersion[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([isActive])
@@index([category])
}
model PolicyVersion {
id String @id @default(uuid())
policyId String
policy Policy @relation(fields: [policyId], references: [id], onDelete: CASCADE)
version Int
content String
publishedAt DateTime?
createdAt DateTime @default(now())
@@index([policyId])
}
model PolicyAcknowledgment {
id String @id @default(uuid())
policyId String
policy Policy @relation(fields: [policyId], references: [id], onDelete: CASCADE)
userId String
user User @relation("PolicyAcknowledgments", fields: [userId], references: [id], onDelete: CASCADE)
version Int
acknowledgedAt DateTime @default(now())
@@unique([policyId, userId, version])
@@index([policyId])
@@index([userId])
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment