Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Affiche le nb de congés payés restants #442

Merged
merged 2 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { IQuery } from 'src/Application/IQuery';

export class GetLeaveRequestsOverviewQuery implements IQuery {
constructor(public readonly date: Date, public readonly userId: string) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { mock, instance, when, verify } from 'ts-mockito';
import { DateUtilsAdapter } from 'src/Infrastructure/Adapter/DateUtilsAdapter';
import { LeaveRepository } from 'src/Infrastructure/HumanResource/Leave/Repository/LeaveRepository';
import { GetLeaveRequestsOverviewQueryHandler } from './GetLeaveRequestsOverviewQueryHandler';
import { ILeaveRequestsOverview } from 'src/Domain/HumanResource/Leave/ILeaveRequestsOverview';
import { GetLeaveRequestsOverviewQuery } from './GetLeaveRequestsOverviewQuery';

describe('GetLeaveRequestsOverviewQueryHandler', () => {
let leaveRepository: LeaveRepository;
let dateUtils: DateUtilsAdapter;
let queryHandler: GetLeaveRequestsOverviewQueryHandler;

beforeEach(() => {
leaveRepository = mock(LeaveRepository);
dateUtils = mock(DateUtilsAdapter);
queryHandler = new GetLeaveRequestsOverviewQueryHandler(
instance(dateUtils),
instance(leaveRepository)
);
});

it('testGetLeaveRequestsOverview', async () => {
const expectedResult: ILeaveRequestsOverview = {
daysPerYear: 35,
daysRemaining: 29
};

const now = new Date('2024-07-05');
const startDate = new Date('2024-06-01');
const endDate = new Date('2025-05-31');
const userId = '06691061-b62f-7499-8000-a65d631224f1';

when(dateUtils.getLeaveReferencePeriodDays(now)).thenReturn([
startDate,
endDate
]);
when(dateUtils.getWorkedDaysPerWeek()).thenReturn(5);
when(dateUtils.getNumberOfPaidLeaveWeeks()).thenReturn(7);

when(
leaveRepository.getSumOfPaidLeaveDurationsBetween(
startDate,
endDate,
userId
)
).thenResolve(6 * 420);

when(dateUtils.getLeaveDurationAsDays(6 * 420)).thenReturn(6);

expect(
await queryHandler.execute(new GetLeaveRequestsOverviewQuery(now, userId))
).toMatchObject(expectedResult);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Inject } from '@nestjs/common';
import { QueryHandler } from '@nestjs/cqrs';
import { GetLeaveRequestsOverviewQuery } from './GetLeaveRequestsOverviewQuery';
import { ILeaveRequestsOverview } from 'src/Domain/HumanResource/Leave/ILeaveRequestsOverview';
import { IDateUtils } from 'src/Application/IDateUtils';
import { ILeaveRepository } from 'src/Domain/HumanResource/Leave/Repository/ILeaveRepository';

@QueryHandler(GetLeaveRequestsOverviewQuery)
export class GetLeaveRequestsOverviewQueryHandler {
constructor(
@Inject('IDateUtils')
private readonly dateUtils: IDateUtils,
@Inject('ILeaveRepository')
private readonly leaveRepository: ILeaveRepository
) {}

public async execute(
query: GetLeaveRequestsOverviewQuery
): Promise<ILeaveRequestsOverview> {
const [startDate, endDate] = this.dateUtils.getLeaveReferencePeriodDays(
query.date
);

const numDaysPerWeek = this.dateUtils.getWorkedDaysPerWeek();
const numWeeks = this.dateUtils.getNumberOfPaidLeaveWeeks();
const daysPerYear = numWeeks * numDaysPerWeek;
fuuuzz marked this conversation as resolved.
Show resolved Hide resolved
const totalDurationTaken = await this.leaveRepository.getSumOfPaidLeaveDurationsBetween(
startDate,
endDate,
query.userId
);
const daysTaken = this.dateUtils.getLeaveDurationAsDays(totalDurationTaken);
const daysRemaining = daysPerYear - daysTaken;

return {
daysPerYear,
daysRemaining
};
}
}
4 changes: 4 additions & 0 deletions src/Application/IDateUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ export interface IDateUtils {
getWorkedDaysDuringAPeriod(start: Date, end: Date): Date[];
isAWorkingDay(date: Date): boolean;
getWorkedFreeDays(year: number): Date[];
getWorkedDaysPerWeek(): number;
getNumberOfPaidLeaveWeeks(): number;
getEasterDate(year: number): Date;
getLeaveDuration(
startDate: string,
isStartsAllDay: boolean,
endDate: string,
isEndsAllDay: boolean
): number;
getLeaveDurationAsDays(duration: number);
getLeaveReferencePeriodDays(date: Date): [Date, Date];
addDaysToDate(date: Date, days: number): Date;
getYear(date: Date): number;
getMonth(date: Date): MonthDate;
Expand Down
4 changes: 4 additions & 0 deletions src/Domain/HumanResource/Leave/ILeaveRequestsOverview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface ILeaveRequestsOverview {
daysPerYear: number;
daysRemaining: number;
}
5 changes: 5 additions & 0 deletions src/Domain/HumanResource/Leave/Repository/ILeaveRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,9 @@ export interface ILeaveRepository {
endDate: string
): Promise<number>;
sumOfDurationLeaveByUserAndDate(user: User, date: string): Promise<number>;
getSumOfPaidLeaveDurationsBetween(
startDate: Date,
endDate: Date,
userId: string
): Promise<number>;
}
30 changes: 30 additions & 0 deletions src/Infrastructure/Adapter/DateUtilsAdapter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,4 +168,34 @@ describe('DateUtilsAdapter', () => {
const result = dateUtils.getMonth(date);
expect(result).toStrictEqual(expectedResult);
});

it('getLeaveDurationsAsDays', () => {
const dateUtils = new DateUtilsAdapter();
expect(dateUtils.getLeaveDurationAsDays(0)).toBe(0);
expect(dateUtils.getLeaveDurationAsDays(420)).toBe(1);
expect(dateUtils.getLeaveDurationAsDays(420 / 2)).toBe(0.5);
});

it('getLeaveReferencePeriodDays', () => {
const dateUtils = new DateUtilsAdapter();
expect(
dateUtils.getLeaveReferencePeriodDays(new Date('2024-01-01'))
).toEqual([new Date('2023-06-01'), new Date('2024-05-31')]);
expect(
dateUtils.getLeaveReferencePeriodDays(new Date('2024-05-31'))
).toEqual([new Date('2023-06-01'), new Date('2024-05-31')]);
expect(
dateUtils.getLeaveReferencePeriodDays(new Date('2024-06-01'))
).toEqual([new Date('2024-06-01'), new Date('2025-05-31')]);
});

it('getWorkedDaysPerWeek', () => {
const dateUtils = new DateUtilsAdapter();
expect(dateUtils.getWorkedDaysPerWeek()).toBe(5);
});

it('getNumberOfPaidLeaveWeeks', () => {
const dateUtils = new DateUtilsAdapter();
expect(dateUtils.getNumberOfPaidLeaveWeeks()).toBe(7);
});
});
34 changes: 34 additions & 0 deletions src/Infrastructure/Adapter/DateUtilsAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,4 +207,38 @@ export class DateUtilsAdapter implements IDateUtils {

return duration;
}

/**
* @param duration Duration in minutes
* @returns Duration in days
*/
public getLeaveDurationAsDays(duration: number) {
return duration / 420;
}

getLeaveReferencePeriodDays(date: Date): [Date, Date] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je me demande si c'est vraiment utile d'avoir la date en param de fonction ? En soit la période de référence est sera toujours calculée de la même façon en fonction de la date actuelle ? Je pense qu'on peut s'en affranchir sur toute la chaîne (Query, QueryHandler, etc) et calculer avec now ? Ou alors y a un truc que j'ai pas compris xD

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C'est pour les tests

Dans le code de DateUtilsAdapter on ne fait jamais new Date('now'), on prend une date de l'extérieur

Car sinon les tests ne sont pas fixés... Par ex ici j'aurais comme résultat 1er juin 2024 / 31 mai 2025, mais le 1er juin 2025 la CI sera KO car le test ne passera plus

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

D'acc, dans ce cas on pourrait peut aussi mettre une valeur par défaut pour ce param

En vrai c'est pas hyper important, je te laisse juge :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vu qu'on fait déjà comme ça ailleurs je vais aller dans le sens de la continuité... et laisser comme ça

Même si je serais OK pour passer sur cette approche de "par défaut on prend now mais pour tester tu peux passer une date spécifique"

// Reference period is between June 1st and May 31st.

let startDate = this.makeDate(this.getYear(date), 6, 1);

if (startDate > date) {
startDate = this.makeDate(this.getYear(date) - 1, 6, 1);
}

let endDate = this.makeDate(this.getYear(date), 5, 31);

if (endDate < date) {
endDate = this.makeDate(this.getYear(date) + 1, 5, 31);
}

return [startDate, endDate];
}

getWorkedDaysPerWeek(): number {
return 5;
}

getNumberOfPaidLeaveWeeks(): number {
return 7;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,54 @@ import { User } from 'src/Domain/HumanResource/User/User.entity';
import { GetLeaveRequestsQuery } from 'src/Application/HumanResource/Leave/Query/GetLeaveRequestsQuery';
import { IsAuthenticatedGuard } from 'src/Infrastructure/HumanResource/User/Security/IsAuthenticatedGuard';
import { WithName } from 'src/Infrastructure/Common/ExtendedRouting/WithName';
import { PaginationDTO } from 'src/Infrastructure/Common/DTO/PaginationDTO';
import { Pagination } from 'src/Application/Common/Pagination';
import { LeaveRequestView } from 'src/Application/HumanResource/Leave/View/LeaveRequestView';
import { LoggedUser } from '../../User/Decorator/LoggedUser';
import { LeaveRequestTableFactory } from '../Table/LeaveRequestTableFactory';
import { LeaveRequestsOverviewTableFactory } from '../Table/LeaveRequestOverviewTableFactory';
import { GetLeaveRequestsOverviewQuery } from 'src/Application/HumanResource/Leave/Query/GetLeaveRequestsOverviewQuery';
import { UserView } from 'src/Application/HumanResource/User/View/UserView';
import { GetUsersQuery } from 'src/Application/HumanResource/User/Query/GetUsersQuery';
import { ListLeaveRequestsControllerDTO } from '../DTO/ListLeaveRequestsControllerDTO';

@Controller('app/people/leave_requests')
@UseGuards(IsAuthenticatedGuard)
export class ListLeaveRequestsController {
constructor(
@Inject('IQueryBus')
private readonly queryBus: IQueryBus,
private readonly tableFactory: LeaveRequestTableFactory
private readonly tableFactory: LeaveRequestTableFactory,
private readonly overviewTableFactory: LeaveRequestsOverviewTableFactory
) {}

@Get()
@WithName('people_leave_requests_list')
@Render('pages/leave_requests/list.njk')
public async get(
@Query() paginationDto: PaginationDTO,
@Query() dto: ListLeaveRequestsControllerDTO,
@LoggedUser() user: User
) {
const pagination: Pagination<LeaveRequestView> = await this.queryBus.execute(
new GetLeaveRequestsQuery(user.getId(), paginationDto.page)
new GetLeaveRequestsQuery(user.getId(), dto.page)
);

const table = this.tableFactory.create(pagination.items, user.getId());

return { table, pagination, currentPage: paginationDto.page };
const userId = dto.userId ? dto.userId : user['id'];

const overview = await this.queryBus.execute(
new GetLeaveRequestsOverviewQuery(new Date(), userId)
);
const overviewTable = await this.overviewTableFactory.create(
overview,
userId
);

return {
table,
overviewTable,
pagination,
currentPage: dto.page
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { IsUUID, IsOptional } from 'class-validator';
import { PaginationDTO } from 'src/Infrastructure/Common/DTO/PaginationDTO';

export class ListLeaveRequestsControllerDTO extends PaginationDTO {
@IsUUID()
@IsOptional()
public userId?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { Leave } from 'src/Domain/HumanResource/Leave/Leave.entity';
import { User, UserRole } from 'src/Domain/HumanResource/User/User.entity';
import { IDateUtils } from 'src/Application/IDateUtils';
import { Type } from 'src/Domain/HumanResource/Leave/LeaveRequest.entity';

export class LeaveRepository implements ILeaveRepository {
constructor(
Expand Down Expand Up @@ -68,4 +69,23 @@

return Number(result.time) || 0;
}

public async getSumOfPaidLeaveDurationsBetween(
startDate: Date,
endDate: Date,
userId: string

Check warning on line 76 in src/Infrastructure/HumanResource/Leave/Repository/LeaveRepository.ts

View check run for this annotation

Codecov / codecov/patch

src/Infrastructure/HumanResource/Leave/Repository/LeaveRepository.ts#L76

Added line #L76 was not covered by tests
): Promise<number> {
const result = await this.repository

Check warning on line 78 in src/Infrastructure/HumanResource/Leave/Repository/LeaveRepository.ts

View check run for this annotation

Codecov / codecov/patch

src/Infrastructure/HumanResource/Leave/Repository/LeaveRepository.ts#L78

Added line #L78 was not covered by tests
.createQueryBuilder('leave')
.select('SUM(leave.time) as time')
.innerJoin('leave.leaveRequest', 'leaveRequest')
.innerJoin('leaveRequest.user', 'user')
.where('leave.date >= :startDate', { startDate })
.andWhere('leave.date < :endDate', { endDate })
.andWhere('leaveRequest.type = :type', { type: Type.PAID })
.andWhere('user.id = :userId', { userId: userId })
.getRawOne();

return Number(result.time) || 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Inject, Injectable } from '@nestjs/common';
import { IQueryBus } from '@nestjs/cqrs';
import { GetUsersQuery } from 'src/Application/HumanResource/User/Query/GetUsersQuery';
import { UserView } from 'src/Application/HumanResource/User/View/UserView';
import { ILeaveRequestsOverview } from 'src/Domain/HumanResource/Leave/ILeaveRequestsOverview';
import { Table } from 'src/Infrastructure/Tables';
import { HtmlColumn } from 'src/Infrastructure/Tables/HtmlColumn';
import { RowFactory } from 'src/Infrastructure/Tables/RowFactory';
import { ITemplates } from 'src/Infrastructure/Templates/ITemplates';

@Injectable()
export class LeaveRequestsOverviewTableFactory {
constructor(
@Inject('IQueryBus')
private queryBus: IQueryBus,
private rowFactory: RowFactory,
@Inject('ITemplates')
private templates: ITemplates
) {}

public async create(
overview: ILeaveRequestsOverview,
userId: string
): Promise<Table> {
const users: UserView[] = await this.queryBus.execute(
new GetUsersQuery(false, true)
);

const columns = [
'leaves-user',
new HtmlColumn(
'leaves-overview-daysRemaining',
this.templates.render(
'pages/leave_requests/overview/days_remaining_column_header.njk',
{
daysPerYear: overview.daysPerYear
}
)
)
];

const row = this.rowFactory
.createBuilder()
.template('pages/leave_requests/overview/user_cell.njk', {
users,
userId
})
.template('pages/leave_requests/overview/days_remaining_cell.njk', {
daysRemaining: overview.daysRemaining
})
.build();

return new Table(columns, [row]);
}
}
6 changes: 6 additions & 0 deletions src/Infrastructure/HumanResource/humanResource.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ import { ListMealTicketsController } from './MealTicket/Controller/ListMealTicke
import { MealTicketTableFactory } from './MealTicket/Table/MealTicketTableFactory';
import { AddMealTicketRemovalController } from './MealTicket/Controller/AddMealTicketRemovalController';
import { ExportLeavesCalendarController } from './Leave/Controller/ExportLeavesCalendarController';
import { LeaveRequestsOverviewTableFactory } from './Leave/Table/LeaveRequestOverviewTableFactory';
import { GetLeaveRequestsOverviewQueryHandler } from 'src/Application/HumanResource/Leave/Query/GetLeaveRequestsOverviewQueryHandler';
import { TemplatesModule } from '../Templates/templates.module';

@Module({
imports: [
Expand All @@ -100,6 +103,7 @@ import { ExportLeavesCalendarController } from './Leave/Controller/ExportLeavesC
UserSavingsRecord,
InterestRate
]),
TemplatesModule,
ExtendedRoutingModule,
TablesModule
],
Expand Down Expand Up @@ -183,6 +187,8 @@ import { ExportLeavesCalendarController } from './Leave/Controller/ExportLeavesC
IncreaseUserSavingsRecordCommandHandler,
UserTableFactory,
LeaveRequestTableFactory,
GetLeaveRequestsOverviewQueryHandler,
LeaveRequestsOverviewTableFactory,
PayrollElementsTableFactory,
MealTicketTableFactory
]
Expand Down
Loading
Loading