Merge branch 'dev/setup/prisma/archive/MatthieuH' of git.targo.ca:Targo/targo_backend

This commit is contained in:
Matthieu Haineault 2025-07-29 16:35:16 -04:00
commit 469e4c857b
20 changed files with 1707 additions and 1318 deletions

File diff suppressed because it is too large Load Diff

38
package-lock.json generated
View File

@ -15,6 +15,7 @@
"@nestjs/jwt": "^11.0.0", "@nestjs/jwt": "^11.0.0",
"@nestjs/passport": "^11.0.5", "@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1", "@nestjs/platform-express": "^11.0.1",
"@nestjs/schedule": "^6.0.0",
"@nestjs/swagger": "^11.2.0", "@nestjs/swagger": "^11.2.0",
"@prisma/client": "^6.11.1", "@prisma/client": "^6.11.1",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
@ -2460,6 +2461,18 @@
"@nestjs/core": "^11.0.0" "@nestjs/core": "^11.0.0"
} }
}, },
"node_modules/@nestjs/schedule": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-6.0.0.tgz",
"integrity": "sha512-aQySMw6tw2nhitELXd3EiRacQRgzUKD9mFcUZVOJ7jPLqIBvXOyvRWLsK9SdurGA+jjziAlMef7iB5ZEFFoQpw==",
"dependencies": {
"cron": "4.3.0"
},
"peerDependencies": {
"@nestjs/common": "^10.0.0 || ^11.0.0",
"@nestjs/core": "^10.0.0 || ^11.0.0"
}
},
"node_modules/@nestjs/schematics": { "node_modules/@nestjs/schematics": {
"version": "11.0.5", "version": "11.0.5",
"resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.5.tgz", "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.5.tgz",
@ -3349,6 +3362,11 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/luxon": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.2.tgz",
"integrity": "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw=="
},
"node_modules/@types/methods": { "node_modules/@types/methods": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
@ -5577,6 +5595,18 @@
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true "dev": true
}, },
"node_modules/cron": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/cron/-/cron-4.3.0.tgz",
"integrity": "sha512-ciiYNLfSlF9MrDqnbMdRWFiA6oizSF7kA1osPP9lRzNu0Uu+AWog1UKy7SkckiDY2irrNjeO6qLyKnXC8oxmrw==",
"dependencies": {
"@types/luxon": "~3.6.0",
"luxon": "~3.6.0"
},
"engines": {
"node": ">=18.x"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -8401,6 +8431,14 @@
"yallist": "^3.0.2" "yallist": "^3.0.2"
} }
}, },
"node_modules/luxon": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz",
"integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==",
"engines": {
"node": ">=12"
}
},
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.17", "version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",

View File

@ -26,6 +26,7 @@
"@nestjs/jwt": "^11.0.0", "@nestjs/jwt": "^11.0.0",
"@nestjs/passport": "^11.0.5", "@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1", "@nestjs/platform-express": "^11.0.1",
"@nestjs/schedule": "^6.0.0",
"@nestjs/swagger": "^11.2.0", "@nestjs/swagger": "^11.2.0",
"@prisma/client": "^6.11.1", "@prisma/client": "^6.11.1",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "shifts_archive" ALTER COLUMN "archive_at" SET DEFAULT CURRENT_TIMESTAMP;

View File

@ -43,7 +43,7 @@ model Employees {
supervisor Employees? @relation("EmployeeSupervisor", fields: [supervisor_id], references: [id]) supervisor Employees? @relation("EmployeeSupervisor", fields: [supervisor_id], references: [id])
supervisor_id Int? supervisor_id Int?
managed_employees Employees[] @relation("EmployeeSupervisor") crew Employees[] @relation("EmployeeSupervisor")
archive EmployeesArchive[] @relation("EmployeeToArchive") archive EmployeesArchive[] @relation("EmployeeToArchive")
timesheet Timesheets[] @relation("TimesheetEmployee") timesheet Timesheets[] @relation("TimesheetEmployee")
@ -183,7 +183,7 @@ model ShiftsArchive {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
shift Shifts @relation("ShiftsToArchive", fields: [shift_id], references: [id]) shift Shifts @relation("ShiftsToArchive", fields: [shift_id], references: [id])
shift_id Int shift_id Int
archive_at DateTime archive_at DateTime @default(now())
timesheet_id Int timesheet_id Int
shift_code_id Int shift_code_id Int
description String? description String?

View File

@ -16,9 +16,13 @@ import { AuthenticationModule } from './modules/authentication/auth.module';
import { ExpensesModule } from './modules/expenses/expenses.module'; import { ExpensesModule } from './modules/expenses/expenses.module';
import { ExpenseCodesModule } from './modules/expense-codes/expense-codes.module'; import { ExpenseCodesModule } from './modules/expense-codes/expense-codes.module';
import { PayperiodsModule } from './modules/pay-periods/pay-periods.module'; import { PayperiodsModule } from './modules/pay-periods/pay-periods.module';
import { ScheduleModule } from '@nestjs/schedule';
import { ArchivalModule } from './modules/archival/archival.module';
@Module({ @Module({
imports: [ imports: [
ScheduleModule.forRoot(),
ArchivalModule,
PrismaModule, PrismaModule,
HealthModule, HealthModule,
UsersModule, UsersModule,

View File

@ -1,4 +1,9 @@
import 'reflect-metadata'; import 'reflect-metadata';
//import and if case for @nestjs/schedule Cron jobs
import * as nodeCrypto from 'crypto';
if(!(globalThis as any).crypto) {
(globalThis as any).crypto = nodeCrypto;
}
import { ModuleRef, NestFactory, Reflector } from '@nestjs/core'; import { ModuleRef, NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common'; import { ValidationPipe } from '@nestjs/common';

View File

@ -0,0 +1,20 @@
import { Module } from "@nestjs/common";
import { ScheduleModule } from "@nestjs/schedule";
import { TimesheetsModule } from "../timesheets/timesheets.module";
import { ExpensesModule } from "../expenses/expenses.module";
import { ShiftsModule } from "../shifts/shifts.module";
import { LeaveRequestsModule } from "../leave-requests/leave-requests.module";
import { ArchivalService } from "./services/archival.service";
@Module({
imports: [
ScheduleModule,
TimesheetsModule,
ExpensesModule,
ShiftsModule,
LeaveRequestsModule,
],
providers: [ArchivalService],
})
export class ArchivalModule {}

View File

@ -0,0 +1,41 @@
import { Injectable, Logger } from "@nestjs/common";
import { Cron } from "@nestjs/schedule";
import { ApiInternalServerErrorResponse, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
import { ExpensesService } from "src/modules/expenses/services/expenses.service";
import { LeaveRequestsService } from "src/modules/leave-requests/services/leave-request.service";
import { ShiftsService } from "src/modules/shifts/services/shifts.service";
import { TimesheetsService } from "src/modules/timesheets/services/timesheets.service";
@Injectable()
export class ArchivalService {
private readonly logger = new Logger(ArchivalService.name);
constructor(
private readonly timesheetsService: TimesheetsService,
private readonly expensesService: ExpensesService,
private readonly shiftsService: ShiftsService,
private readonly leaveRequestsService: LeaveRequestsService,
) {}
@Cron('0 0 3 * * 1', {timeZone:'America/Toronto'}) // chaque premier lundi du mois à 03h00
async handleMonthlyArchival() {
const today = new Date();
const dayOfMonth = today.getDate();
if (dayOfMonth > 7) {
this.logger.warn('Archive {awaiting 1st monday of the month for archivation process}')
return;
}
this.logger.log('monthly archivation in process');
try {
await this.timesheetsService.archiveOld();
await this.expensesService.archiveOld();
await this.shiftsService.archiveOld();
await this.leaveRequestsService.archiveExpired();
this.logger.log('archivation process done');
} catch (err) {
this.logger.error('an error occured during archivation process ', err);
}
}
}

View File

@ -1,10 +1,10 @@
import { Body,Controller,Delete,Get,Param,ParseIntPipe,Patch,Post,UseGuards } from '@nestjs/common'; import { Body,Controller,Delete,Get,NotFoundException,Param,ParseIntPipe,Patch,Post,UseGuards } from '@nestjs/common';
import { Employees, Roles as RoleEnum } from '@prisma/client'; import { Employees, Roles as RoleEnum } from '@prisma/client';
import { EmployeesService } from '../services/employees.service'; import { EmployeesService } from '../services/employees.service';
import { CreateEmployeeDto } from '../dtos/create-employee.dto'; import { CreateEmployeeDto } from '../dtos/create-employee.dto';
import { UpdateEmployeeDto } from '../dtos/update-employee.dto'; import { UpdateEmployeeDto } from '../dtos/update-employee.dto';
import { RolesAllowed } from '../../../common/decorators/roles.decorators'; import { RolesAllowed } from '../../../common/decorators/roles.decorators';
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiBearerAuth, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { JwtAuthGuard } from 'src/modules/authentication/guards/jwt-auth.guard'; import { JwtAuthGuard } from 'src/modules/authentication/guards/jwt-auth.guard';
import { EmployeeEntity } from '../dtos/swagger-entities/employees.entity'; import { EmployeeEntity } from '../dtos/swagger-entities/employees.entity';
@ -27,7 +27,7 @@ export class EmployeesController {
@Get() @Get()
@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ACCOUNTING) @RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ACCOUNTING)
@ApiOperation({summary: 'Find all employees' }) @ApiOperation({summary: 'Find all employees' })
@ApiResponse({ status: 201, description: 'List of employees found', type: EmployeeEntity, isArray: true }) @ApiResponse({ status: 200, description: 'List of employees found', type: EmployeeEntity, isArray: true })
@ApiResponse({ status: 400, description: 'List of employees not found' }) @ApiResponse({ status: 400, description: 'List of employees not found' })
findAll(): Promise<Employees[]> { findAll(): Promise<Employees[]> {
return this.employeesService.findAll(); return this.employeesService.findAll();
@ -36,30 +36,39 @@ export class EmployeesController {
@Get(':id') @Get(':id')
@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR,RoleEnum.ACCOUNTING ) @RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR,RoleEnum.ACCOUNTING )
@ApiOperation({summary: 'Find employee' }) @ApiOperation({summary: 'Find employee' })
@ApiResponse({ status: 201, description: 'Employee found', type: EmployeeEntity }) @ApiResponse({ status: 200, description: 'Employee found', type: EmployeeEntity })
@ApiResponse({ status: 400, description: 'Employee not found' }) @ApiResponse({ status: 400, description: 'Employee not found' })
findOne(@Param('id', ParseIntPipe) id: number): Promise<Employees> { findOne(@Param('id', ParseIntPipe) id: number): Promise<Employees> {
return this.employeesService.findOne(id); return this.employeesService.findOne(id);
} }
@Patch(':id')
@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR,RoleEnum.ACCOUNTING )
@ApiOperation({summary: 'Update employee' })
@ApiResponse({ status: 201, description: 'Employee updated', type: EmployeeEntity })
@ApiResponse({ status: 400, description: 'Employee not found' })
update(
@Param('id', ParseIntPipe) id: number,
@Body() dto: UpdateEmployeeDto,
): Promise<Employees> {
return this.employeesService.update(id, dto);
}
@Delete(':id') @Delete(':id')
@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR ) @RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR )
@ApiOperation({summary: 'Delete employee' }) @ApiOperation({summary: 'Delete employee' })
@ApiResponse({ status: 201, description: 'Employee deleted', type: EmployeeEntity }) @ApiParam({ name: 'id', type: Number, description: 'Identifier of the employee to delete' })
@ApiResponse({ status: 400, description: 'Employee not found' }) @ApiResponse({ status: 204, description: 'Employee deleted' })
@ApiResponse({ status: 404, description: 'Employee not found' })
remove(@Param('id', ParseIntPipe) id: number): Promise<Employees> { remove(@Param('id', ParseIntPipe) id: number): Promise<Employees> {
return this.employeesService.remove(id); return this.employeesService.remove(id);
} }
@Patch(':id')
@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
@ApiBearerAuth('access-token')
@ApiOperation({ summary: 'Update, archive or restore an employee' })
@ApiParam({ name: 'id', type: Number, description: 'Identifier of the employee' })
@ApiResponse({ status: 200, description: 'Employee updated or restored', type: EmployeeEntity })
@ApiResponse({ status: 202, description: 'Employee archived successfully', type: EmployeeEntity })
@ApiResponse({ status: 404, description: 'Employee not found in active or archive' })
@Patch(':id')
async updateOrArchiveOrRestore(@Param('id') id: string, @Body() dto: UpdateEmployeeDto,) {
// if last_work_day is set => archive the employee
// else if employee is archived and first_work_day or last_work_day = null => restore
//otherwise => standard update
const result = await this.employeesService.patchEmployee(+id, dto);
if(!result) {
throw new NotFoundException(`Employee #${ id } not found in active or archive.`)
}
return result;
}
} }

View File

@ -1,4 +1,13 @@
import { PartialType } from '@nestjs/swagger'; import { ApiProperty, PartialType } from '@nestjs/swagger';
import { CreateEmployeeDto } from './create-employee.dto'; import { CreateEmployeeDto } from './create-employee.dto';
export class UpdateEmployeeDto extends PartialType(CreateEmployeeDto) {} export class UpdateEmployeeDto extends PartialType(CreateEmployeeDto) {
@ApiProperty({ required: false, type: Date, description: 'New hire date or undefined' })
first_work_day?: Date;
@ApiProperty({ required: false, type: Date, description: 'Termination date (null to restore)' })
last_work_day?: Date;
@ApiProperty({ required: false, type: Number, description: 'Supervisor ID' })
supervisor_id?: number;
}

View File

@ -107,4 +107,86 @@ async update(
await this.findOne(id); await this.findOne(id);
return this.prisma.employees.delete({ where: { id } }); return this.prisma.employees.delete({ where: { id } });
} }
//archivation function
async patchEmployee(id: number, dto: UpdateEmployeeDto): Promise<any> {
//fetching existing employee
const existing = await this.prisma.employees.findUnique({
where: { id },
include: { user: true, archive: true },
});
if (existing) {
//verify last_work_day is not null => trigger archivation
if(dto.last_work_day != undefined && existing.last_work_day == null) {
return this.archiveOnTermination(existing, dto);
}
//if null => regular update
return this.prisma.employees.update({
where: { id },
data: dto,
});
}
//if not found => fetch archives side for restoration
const archived = await this.prisma.employeesArchive.findFirst({
where: { employee_id: id },
include: { employee: true, user: true },
});
if (archived) {
//conditions for restoration
const restore = dto.last_work_day === null || dto.first_work_day != null;
if(restore) {
return this.restoreEmployee(archived, dto);
}
}
//if neither activated nor archivated => 404
return null;
}
//transfers the employee to archive and then delete from employees table
private async archiveOnTermination(existing: any, dto: UpdateEmployeeDto): Promise<any> {
return this.prisma.$transaction(async transaction => {
//archive insertion
const archived = await transaction.employeesArchive.create({
data: {
employee_id: existing.id,
user_id: existing.user_id,
first_name: existing.first_name,
last_name: existing.last_name,
external_payroll_id: existing.external_payroll_id,
company_code: existing.company_code,
first_Work_Day: existing.first_Work_Day,
last_work_day: existing.last_work_day,
supervisor_id: existing.supervisor_id ?? null,
},
});
//delete from employees table
await transaction.employees.delete({ where: { id: existing.id } });
//return archived employee
return archived
});
}
//transfers the employee from archive to the employees table
private async restoreEmployee(archived: any, dto: UpdateEmployeeDto): Promise<any> {
return this.prisma.$transaction(async transaction => {
//restores the archived employee into the employees table
const restored = await transaction.employees.create({
data: {
id: archived.employee_id,
user_id: archived.user_id,
external_payroll_id: dto.external_payroll_id ?? archived.external_payroll_id,
company_code: dto.company_code ?? archived.company_code,
first_work_day: dto.first_work_day ?? archived.first_Work_Day,
last_work_day: null,
supervisor_id: dto.supervisor_id ?? archived.supervisor_id,
},
});
//deleting archived entry by id
await transaction.employeesArchive.delete({ where: { id: archived.id } });
//return restored employee
return restored;
});
}
} }

View File

@ -5,7 +5,8 @@ import { ExpensesService } from "./services/expenses.service";
@Module({ @Module({
controllers: [ExpensesController], controllers: [ExpensesController],
providers: [ExpensesService, PrismaService] providers: [ExpensesService, PrismaService],
exports: [ ExpensesService ],
}) })
export class ExpensesModule {} export class ExpensesModule {}

View File

@ -86,4 +86,50 @@ export class ExpensesService {
await this.findOne(id); await this.findOne(id);
return this.prisma.expenses.delete({ where: { id } }); return this.prisma.expenses.delete({ where: { id } });
} }
//archivation function
async archiveOld(): Promise<void> {
//fetches archived timesheet's Ids
const archivedTimesheets = await this.prisma.timesheetsArchive.findMany({
select: { timesheet_id: true },
});
const timesheetIds = archivedTimesheets.map(sheet => sheet.timesheet_id);
if(timesheetIds.length === 0) {
return;
}
// copy/delete transaction
await this.prisma.$transaction(async transaction => {
//fetches expenses to move to archive
const expensesToArchive = await transaction.expenses.findMany({
where: { timesheet_id: { in: timesheetIds } },
});
if(expensesToArchive.length === 0) {
return;
}
//copies sent to archive table
await transaction.expensesArchive.createMany({
data: expensesToArchive.map(exp => ({
expense_id: exp.id,
timesheet_id: exp.timesheet_id,
expense_code_id: exp.expense_code_id,
date: exp.date,
amount: exp.amount,
attachement: exp.attachement,
description: exp.description,
is_approved: exp.is_approved,
supervisor_comment: exp.supervisor_comment,
})),
});
//delete from expenses table
await transaction.expenses.deleteMany({
where: { id: { in: expensesToArchive.map(exp => exp.id) } },
})
})
}
} }

View File

@ -6,6 +6,7 @@ import { Module } from "@nestjs/common";
@Module({ @Module({
controllers: [LeaveRequestController], controllers: [LeaveRequestController],
providers: [ LeaveRequestsService, PrismaService], providers: [ LeaveRequestsService, PrismaService],
exports: [ LeaveRequestsService],
}) })
export class LeaveRequestsModule {} export class LeaveRequestsModule {}

View File

@ -106,4 +106,36 @@ export class LeaveRequestsService {
where: { id }, where: { id },
}); });
} }
//archivation function
async archiveExpired(): Promise<void> {
const now = new Date();
await this.prisma.$transaction(async transaction => {
//fetches expired leave requests
const expired = await transaction.leaveRequests.findMany({
where: { end_date_time: { lt: now } },
});
if(expired.length === 0) {
return;
}
//copy unto archive table
await transaction.leaveRequestsArchive.createMany({
data: expired.map(request => ({
leave_request_id: request.id,
employee_id: request.employee_id,
leave_type: request.leave_type,
start_date_time: request.start_date_time,
end_date_time: request.end_date_time,
comment: request.comment,
approval_status: request.approval_status,
})),
});
//delete from leave_requests table
await transaction.leaveRequests.deleteMany({
where: { id: { in: expired.map(request => request.id ) } },
});
});
}
} }

View File

@ -86,4 +86,48 @@ export class ShiftsService {
await this.findOne(id); await this.findOne(id);
return this.prisma.shifts.delete({ where: { id } }); return this.prisma.shifts.delete({ where: { id } });
} }
//archivation function
async archiveOld(): Promise<void> {
//fetches archived timesheet's Ids
const archivedTimesheets = await this.prisma.timesheetsArchive.findMany({
select: { timesheet_id: true },
});
const timesheetIds = archivedTimesheets.map(sheet => sheet.timesheet_id);
if(timesheetIds.length === 0) {
return;
}
// copy/delete transaction
await this.prisma.$transaction(async transaction => {
//fetches shifts to move to archive
const shiftsToArchive = await transaction.shifts.findMany({
where: { timesheet_id: { in: timesheetIds } },
});
if(shiftsToArchive.length === 0) {
return;
}
//copies sent to archive table
await transaction.shiftsArchive.createMany({
data: shiftsToArchive.map(shift => ({
shift_id: shift.id,
timesheet_id: shift.timesheet_id,
shift_code_id: shift.shift_code_id,
description: shift.description ?? undefined,
date: shift.date,
start_time: shift.start_time,
end_time: shift.end_time,
})),
});
//delete from shifts table
await transaction.shifts.deleteMany({
where: { id: { in: shiftsToArchive.map(shift => shift.id) } },
})
})
}
} }

View File

@ -5,6 +5,7 @@ import { PrismaService } from 'src/prisma/prisma.service';
@Module({ @Module({
controllers: [ShiftsController], controllers: [ShiftsController],
providers: [ShiftsService, PrismaService] providers: [ShiftsService, PrismaService],
exports: [ShiftsService],
}) })
export class ShiftsModule {} export class ShiftsModule {}

View File

@ -75,4 +75,44 @@ export class TimesheetsService {
} }
//Archivation function
async archiveOld(): Promise<void> {
//calcul du cutoff pour archivation
const cutoff = new Date();
cutoff.setMonth(cutoff.getMonth() - 6)
await this.prisma.$transaction(async transaction => {
//fetches all timesheets to cutoff
const oldSheets = await transaction.timesheets.findMany({
where: { shift: { every: { date: { lt: cutoff } },
},
},
select: {
id: true,
employee_id: true,
is_approved: true,
},
});
if( oldSheets.length === 0) {
return;
}
//preping data for archivation
const archiveDate = oldSheets.map(sheet => ({
timesheet_id: sheet.id,
employee_id: sheet.employee_id,
is_approved: sheet.is_approved,
}));
//copying data from timesheets table to archive table
await transaction.timesheetsArchive.createMany({
data: archiveDate,
});
//removing data from timesheets table
await transaction.timesheets.deleteMany({
where: { id: { in: oldSheets.map(s => s.id) } },
});
});
}
} }

View File

@ -4,6 +4,7 @@ import { TimesheetsService } from './services/timesheets.service';
@Module({ @Module({
controllers: [TimesheetsController], controllers: [TimesheetsController],
providers: [TimesheetsService] providers: [TimesheetsService],
exports: [TimesheetsService],
}) })
export class TimesheetsModule {} export class TimesheetsModule {}