Skip to content

Commit

Permalink
Merge pull request #188 from fairnesscoop/feat/remove-leaev-request
Browse files Browse the repository at this point in the history
Remove leave request
  • Loading branch information
mmarchois authored Mar 6, 2021
2 parents e65836c + ae3d83e commit fa4d5c6
Show file tree
Hide file tree
Showing 14 changed files with 288 additions and 4 deletions.
1 change: 1 addition & 0 deletions client/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@
"moderate_by": "Modéré par",
"end_date": "Date de fin",
"start_date": "Date de début",
"confirm": "Etes-vous sûr de vouloir supprimer cette demande de congé ?",
"add": {
"title": "Faire une demande de congé"
},
Expand Down
21 changes: 21 additions & 0 deletions client/src/components/links/DeleteLink.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script>
import { _ } from 'svelte-i18n';
import { createEventDispatcher } from 'svelte';
import TrashIcon from '../icons/TrashIcon.svelte';
export let confirmMessage = 'common.confirm.remove';
const dispatch = createEventDispatcher();
const handleClick = () => {
if (confirm($_(confirmMessage))) {
dispatch('confirm');
}
}
</script>

<button
on:click={handleClick}
class="flex items-center justify-between px-2 text-sm font-medium leading-5 text-purple-600 rounded-lg dark:text-gray-400 focus:outline-none focus:shadow-outline-gray"
aria-label={$_('common.form.remove')}>
<TrashIcon className={'w-5 h-5'} />
</button>
13 changes: 12 additions & 1 deletion client/src/routes/human_resources/leaves/requests/_Table.svelte
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
<script>
import { stores } from '@sapper/app';
import { format } from 'date-fns';
import { createEventDispatcher } from 'svelte';
import { _ } from 'svelte-i18n';
import SeeLink from '../../../../components/links/SeeLink.svelte';
import RedBadge from '../../../../components/badges/RedBadge.svelte';
import OrangeBadge from '../../../../components/badges/OrangeBadge.svelte';
import GrayBadge from '../../../../components/badges/GrayBadge.svelte';
import DeleteLink from '../../../../components/links/DeleteLink.svelte';
export let items;
const formatDate = (date) => {
return format(new Date(date), 'dd/MM/yyyy');
};
export let items;
const dispatch = createEventDispatcher();
const { session } = stores();
</script>

<table class="w-full whitespace-no-wrap">
Expand Down Expand Up @@ -53,6 +59,11 @@
<td class="px-4 py-3">
<div class="flex items-center space-x-4 text-sm">
<SeeLink href={`/human_resources/leaves/requests/${id}`} />
{#if $session.user && $session.user.id === user.id}
<DeleteLink
on:confirm={() => dispatch('delete', id)}
confirmMessage={$_("human_resources.leaves.requests.confirm")} />
{/if}
</div>
</td>
</tr>
Expand Down
15 changes: 13 additions & 2 deletions client/src/routes/human_resources/leaves/requests/index.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<script>
import { onMount } from 'svelte';
import { _ } from 'svelte-i18n';
import { get } from '../../../../utils/axios';
import { get, del } from '../../../../utils/axios';
import { historyPushState } from '../../../../utils/url';
import { errorNormalizer } from '../../../../normalizer/errors';
import H4Title from '../../../../components/H4Title.svelte';
Expand Down Expand Up @@ -39,6 +39,17 @@
fetchLeaveRequests();
};
const handleDelete = async (event) => {
const id = event.detail;
try {
await del(`leave-requests/${id}`);
response.items = response.items.filter((leaveRequest) => leaveRequest.id !== id);
} catch (e) {
errors = errorNormalizer(e);
}
}
const fetchLeaveRequests = async () => {
try {
response = (await get('leave-requests', { params: { page } })).data;
Expand Down Expand Up @@ -67,7 +78,7 @@
</div>
<div class="w-full overflow-hidden rounded-lg shadow-xs">
<div class="w-full overflow-x-auto">
<Table items="{response.items}" />
<Table items="{response.items}" on:delete={handleDelete} />
</div>
<Pagination
on:change="{changePage}"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ICommand } from 'src/Application/ICommand';
import { User } from 'src/Domain/HumanResource/User/User.entity';

export class DeleteLeaveRequestCommand implements ICommand {
constructor(
public readonly id: string,
public readonly owner: User
) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { mock, instance, when, verify, anything, deepEqual } from 'ts-mockito';
import { DeleteLeaveRequestCommandHandler } from './DeleteLeaveRequestCommandHandler';
import { DeleteLeaveRequestCommand } from './DeleteLeaveRequestCommand';
import { User } from 'src/Domain/HumanResource/User/User.entity';
import { LeaveRequestNotFoundException } from 'src/Domain/HumanResource/Leave/Exception/LeaveRequestNotFoundException';
import { CanLeaveRequestBeRemoved } from 'src/Domain/HumanResource/Leave/Specification/CanLeaveRequestBeRemoved';
import { LeaveRequest } from 'src/Domain/HumanResource/Leave/LeaveRequest.entity';
import { LeaveRequestRepository } from 'src/Infrastructure/HumanResource/Leave/Repository/LeaveRequestRepository';
import { LeaveRequestCantBeRemovedException } from 'src/Domain/HumanResource/Leave/Exception/LeaveRequestCantBeRemovedException';

describe('DeleteLeaveRequestCommandHandler', () => {
let leaveRequestRepository: LeaveRequestRepository;
let canLeaveRequestBeRemoved: CanLeaveRequestBeRemoved;
let handler: DeleteLeaveRequestCommandHandler;

const user = mock(User);
const leaveRequest = mock(LeaveRequest);
const command = new DeleteLeaveRequestCommand(
'cfdd06eb-cd71-44b9-82c6-46110b30ce05',
instance(user)
);

beforeEach(() => {
leaveRequestRepository = mock(LeaveRequestRepository);
canLeaveRequestBeRemoved = mock(CanLeaveRequestBeRemoved);

handler = new DeleteLeaveRequestCommandHandler(
instance(leaveRequestRepository),
instance(canLeaveRequestBeRemoved),
);
});

it('testLeaveRequestNotNotFound', async () => {
when(
leaveRequestRepository.findOneById('cfdd06eb-cd71-44b9-82c6-46110b30ce05')
).thenResolve(null);

try {
expect(await handler.execute(command)).toBeUndefined();
} catch (e) {
expect(e).toBeInstanceOf(LeaveRequestNotFoundException);
expect(e.message).toBe('human_resources.leaves.requests.errors.not_found');
verify(
leaveRequestRepository.findOneById('cfdd06eb-cd71-44b9-82c6-46110b30ce05')
).once();
verify(
canLeaveRequestBeRemoved.isSatisfiedBy(anything(), anything())
).never();
verify(leaveRequestRepository.remove(anything())).never();
}
});

it('testLeaveRequestCantBeRemoved', async () => {
when(
leaveRequestRepository.findOneById('cfdd06eb-cd71-44b9-82c6-46110b30ce05')
).thenResolve(instance(leaveRequest));
when(
canLeaveRequestBeRemoved.isSatisfiedBy(instance(leaveRequest), instance(user))
).thenReturn(false);

try {
expect(await handler.execute(command)).toBeUndefined();
} catch (e) {
expect(e).toBeInstanceOf(LeaveRequestCantBeRemovedException);
expect(e.message).toBe('human_resources.leaves.requests.errors.cant_be_removed');
verify(
canLeaveRequestBeRemoved.isSatisfiedBy(instance(leaveRequest), instance(user))
).once();
verify(
leaveRequestRepository.findOneById('cfdd06eb-cd71-44b9-82c6-46110b30ce05')
).once();
verify(leaveRequestRepository.remove(anything())).never();
}
});

it('testLeaveSuccessfullyDeleted', async () => {
when(
leaveRequestRepository.findOneById('cfdd06eb-cd71-44b9-82c6-46110b30ce05')
).thenResolve(instance(leaveRequest));
when(
canLeaveRequestBeRemoved.isSatisfiedBy(instance(leaveRequest), instance(user))
).thenReturn(true);

expect(await handler.execute(command)).toBeUndefined();

verify(
canLeaveRequestBeRemoved.isSatisfiedBy(instance(leaveRequest), instance(user))
).once();
verify(
leaveRequestRepository.findOneById('cfdd06eb-cd71-44b9-82c6-46110b30ce05')
).once();
verify(leaveRequestRepository.remove(instance(leaveRequest))).once();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Inject } from '@nestjs/common';
import { CommandHandler } from '@nestjs/cqrs';
import { DeleteLeaveRequestCommand } from './DeleteLeaveRequestCommand';
import { ILeaveRequestRepository } from 'src/Domain/HumanResource/Leave/Repository/ILeaveRequestRepository';
import { LeaveRequestNotFoundException } from 'src/Domain/HumanResource/Leave/Exception/LeaveRequestNotFoundException';
import { CanLeaveRequestBeRemoved } from 'src/Domain/HumanResource/Leave/Specification/CanLeaveRequestBeRemoved';
import { LeaveRequestCantBeRemovedException } from 'src/Domain/HumanResource/Leave/Exception/LeaveRequestCantBeRemovedException';

@CommandHandler(DeleteLeaveRequestCommand)
export class DeleteLeaveRequestCommandHandler {
constructor(
@Inject('ILeaveRequestRepository')
private readonly leaveRequestRepository: ILeaveRequestRepository,
private readonly canLeaveRequestBeRemoved: CanLeaveRequestBeRemoved
) {}

public async execute(command: DeleteLeaveRequestCommand): Promise<void> {
const { owner, id } = command;

const leaveRequest = await this.leaveRequestRepository.findOneById(id);
if (!leaveRequest) {
throw new LeaveRequestNotFoundException();
}

if (
false === this.canLeaveRequestBeRemoved.isSatisfiedBy(leaveRequest, owner)
) {
throw new LeaveRequestCantBeRemovedException();
}

this.leaveRequestRepository.remove(leaveRequest);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class LeaveRequestCantBeRemovedException extends Error {
constructor() {
super('human_resources.leaves.requests.errors.cant_be_removed');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { User } from '../../User/User.entity';

export interface ILeaveRequestRepository {
save(leaveRequest: LeaveRequest): Promise<LeaveRequest>;
remove(leaveRequest: LeaveRequest): void;
findOneById(id: string): Promise<LeaveRequest | undefined>;
findLeaveRequests(page: number, status?: Status): Promise<[LeaveRequest[], number]>;
findExistingLeaveRequestsByUserAndPeriod(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { mock, instance, when } from 'ts-mockito';
import { User } from 'src/Domain/HumanResource/User/User.entity';
import { LeaveRequest } from '../LeaveRequest.entity';
import { CanLeaveRequestBeRemoved } from './CanLeaveRequestBeRemoved';

describe('CanLeaveRequestBeRemoved', () => {
let canLeaveRequestBeRemoved: CanLeaveRequestBeRemoved;
const user = mock(User);
const leaveRequest = mock(LeaveRequest);
const owner = mock(User);

beforeEach(() => {
canLeaveRequestBeRemoved = new CanLeaveRequestBeRemoved();
});

it('testLeaveRequestCantBeRemoved', async () => {
when(user.getId()).thenReturn('cfdd06eb-cd71-44b9-82c6-46110b30ce05');
when(owner.getId()).thenReturn('50e624ef-3609-4053-a437-f74844a2d2de');
when(leaveRequest.getUser()).thenReturn(instance(user));

expect(
await canLeaveRequestBeRemoved.isSatisfiedBy(
instance(leaveRequest),
instance(owner)
)
).toBe(false);
});

it('testLeaveRequestCanBeRemoved', async () => {
when(user.getId()).thenReturn('cfdd06eb-cd71-44b9-82c6-46110b30ce05');
when(owner.getId()).thenReturn('cfdd06eb-cd71-44b9-82c6-46110b30ce05');
when(leaveRequest.getUser()).thenReturn(instance(user));

expect(
await canLeaveRequestBeRemoved.isSatisfiedBy(
instance(leaveRequest),
instance(owner)
)
).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { User } from '../../User/User.entity';
import { LeaveRequest } from '../LeaveRequest.entity';

export class CanLeaveRequestBeRemoved {
public isSatisfiedBy(leaveRequest: LeaveRequest, user: User): boolean {
return leaveRequest.getUser().getId() === user.getId();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
Controller,
Inject,
BadRequestException,
UseGuards,
Param,
Delete
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
import { IdDTO } from 'src/Infrastructure/Common/DTO/IdDTO';
import { Roles } from 'src/Infrastructure/HumanResource/User/Decorator/Roles';
import { UserRole, User } from 'src/Domain/HumanResource/User/User.entity';
import { RolesGuard } from 'src/Infrastructure/HumanResource/User/Security/RolesGuard';
import { LoggedUser } from '../../User/Decorator/LoggedUser';
import { ICommandBus } from 'src/Application/ICommandBus';
import { DeleteLeaveRequestCommand } from 'src/Application/HumanResource/Leave/Command/DeleteLeaveRequestCommand';

@Controller('leave-requests')
@ApiTags('Human Resource')
@ApiBearerAuth()
@UseGuards(AuthGuard('bearer'), RolesGuard)
export class DeleteLeaveRequestAction {
constructor(
@Inject('ICommandBus')
private readonly commandBus: ICommandBus
) {}

@Delete(':id')
@Roles(UserRole.COOPERATOR)
@ApiOperation({summary: 'Delete leave request'})
public async index(@Param() { id }: IdDTO, @LoggedUser() user: User) {
try {
await this.commandBus.execute(new DeleteLeaveRequestCommand(id, user));
} catch (e) {
throw new BadRequestException(e.message);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export class LeaveRequestRepository implements ILeaveRequestRepository {
return this.repository.save(leaveRequest);
}

public remove(leaveRequest: LeaveRequest): void {
this.repository.delete(leaveRequest.getId());
}

public findOneById(id: string): Promise<LeaveRequest | undefined> {
return this.repository
.createQueryBuilder('leaveRequest')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ import { GetUserAction } from './User/Action/GetUserAction';
import { UpdateUserCommandHandler } from 'src/Application/HumanResource/User/Command/UpdateUserCommandHandler';
import { GetUserAdministrativeByIdQueryHandler } from 'src/Application/HumanResource/User/Query/GetUserAdministrativeByIdQueryHandler';
import { GetLeavesAction } from './Leave/Action/GetLeavesAction';
import { CanLeaveRequestBeRemoved } from 'src/Domain/HumanResource/Leave/Specification/CanLeaveRequestBeRemoved';
import { DeleteLeaveRequestCommandHandler } from 'src/Application/HumanResource/Leave/Command/DeleteLeaveRequestCommandHandler';
import { DeleteLeaveRequestAction } from './Leave/Action/DeleteLeaveRequestAction';

@Module({
imports: [
Expand Down Expand Up @@ -91,7 +94,8 @@ import { GetLeavesAction } from './Leave/Action/GetLeavesAction';
GetLeaveRequestAction,
CreateLeaveRequestAction,
RefuseLeaveRequestAction,
AcceptLeaveRequestAction
AcceptLeaveRequestAction,
DeleteLeaveRequestAction
],
providers: [
{provide: 'IUserRepository', useClass: UserRepository},
Expand Down Expand Up @@ -131,6 +135,8 @@ import { GetLeavesAction } from './Leave/Action/GetLeavesAction';
GetLeaveRequestsQueryHandler,
GetLeaveRequestByIdQueryHandler,
DoesEventsOrLeaveExistForPeriod,
CanLeaveRequestBeRemoved,
DeleteLeaveRequestCommandHandler
]
})
export class HumanResourceModule {}

0 comments on commit fa4d5c6

Please sign in to comment.