diff --git a/api/src/Application/School/Query/User/GetSchoolUsersQueryHandler.spec.ts b/api/src/Application/School/Query/User/GetSchoolUsersQueryHandler.spec.ts index 48dbd22..94b5383 100644 --- a/api/src/Application/School/Query/User/GetSchoolUsersQueryHandler.spec.ts +++ b/api/src/Application/School/Query/User/GetSchoolUsersQueryHandler.spec.ts @@ -4,21 +4,19 @@ import { User } from 'src/Domain/User/User.entity'; import { GetSchoolUsersQuery } from './GetSchoolUsersQuery'; import { GetSchoolUsersQueryHandler } from './GetSchoolUsersQueryHandler'; import { SchoolUserRepository } from 'src/Infrastructure/School/Repository/SchoolUserRepository'; -import { UserSchoolView } from '../../View/UserSchoolView'; +import { UserSummaryView } from 'src/Application/User/View/UserSummaryView'; describe('GetSchoolsQueryHandler', () => { it('testGetSchoolUsers', async () => { const schoolUserRepository = mock(SchoolUserRepository); const user1 = mock(User); + when(user1.getId()).thenReturn('4de2ffc4-e835-44c8-95b7-17c171c09873'); when(user1.getFirstName()).thenReturn('Mathieu'); when(user1.getLastName()).thenReturn('MARCHOIS'); when(user1.getEmail()).thenReturn('mathieu@fairness.coop'); const schoolUser1 = mock(SchoolUser); - when(schoolUser1.getId()).thenReturn( - '4de2ffc4-e835-44c8-95b7-17c171c09873' - ); when(schoolUser1.getUser()).thenReturn(instance(user1)); when( @@ -32,7 +30,7 @@ describe('GetSchoolsQueryHandler', () => { ); const expectedResult = [ - new UserSchoolView( + new UserSummaryView( '4de2ffc4-e835-44c8-95b7-17c171c09873', 'Mathieu', 'MARCHOIS', diff --git a/api/src/Application/School/Query/User/GetSchoolUsersQueryHandler.ts b/api/src/Application/School/Query/User/GetSchoolUsersQueryHandler.ts index 5091ade..b705dca 100644 --- a/api/src/Application/School/Query/User/GetSchoolUsersQueryHandler.ts +++ b/api/src/Application/School/Query/User/GetSchoolUsersQueryHandler.ts @@ -2,7 +2,7 @@ import { QueryHandler } from '@nestjs/cqrs'; import { Inject } from '@nestjs/common'; import { GetSchoolUsersQuery } from './GetSchoolUsersQuery'; import { ISchoolUserRepository } from 'src/Domain/School/Repository/ISchoolUserRepository'; -import { UserSchoolView } from '../../View/UserSchoolView'; +import { UserSummaryView } from 'src/Application/User/View/UserSummaryView'; @QueryHandler(GetSchoolUsersQuery) export class GetSchoolUsersQueryHandler { @@ -11,17 +11,19 @@ export class GetSchoolUsersQueryHandler { private readonly schoolUserRepository: ISchoolUserRepository ) {} - public async execute(query: GetSchoolUsersQuery): Promise { - const userViews: UserSchoolView[] = []; + public async execute(query: GetSchoolUsersQuery): Promise { + const userViews: UserSummaryView[] = []; const schoolUsers = await this.schoolUserRepository.findUsersBySchool(query.schoolId); for (const schoolUser of schoolUsers) { + const user = schoolUser.getUser(); + userViews.push( - new UserSchoolView( - schoolUser.getId(), - schoolUser.getUser().getFirstName(), - schoolUser.getUser().getLastName(), - schoolUser.getUser().getEmail() + new UserSummaryView( + user.getId(), + user.getFirstName(), + user.getLastName(), + user.getEmail() ) ); } diff --git a/api/src/Application/School/View/UserSchoolView.ts b/api/src/Application/School/View/UserSchoolView.ts deleted file mode 100644 index 4b9b515..0000000 --- a/api/src/Application/School/View/UserSchoolView.ts +++ /dev/null @@ -1,8 +0,0 @@ -export class UserSchoolView { - constructor( - public readonly id: string, - public readonly firstName: string, - public readonly lastName: string, - public readonly email: string - ) {} -} diff --git a/api/src/Application/User/Command/RemoveUserCommand.ts b/api/src/Application/User/Command/RemoveUserCommand.ts new file mode 100644 index 0000000..283255e --- /dev/null +++ b/api/src/Application/User/Command/RemoveUserCommand.ts @@ -0,0 +1,8 @@ +import { ICommand } from 'src/Application/ICommand'; + +export class RemoveUserCommand implements ICommand { + constructor( + public readonly id: string, + public readonly currentUserId: string + ) {} +} diff --git a/api/src/Application/User/Command/RemoveUserCommandHandler.spec.ts b/api/src/Application/User/Command/RemoveUserCommandHandler.spec.ts new file mode 100644 index 0000000..0167cc3 --- /dev/null +++ b/api/src/Application/User/Command/RemoveUserCommandHandler.spec.ts @@ -0,0 +1,75 @@ +import { mock, instance, when, verify, anything } from 'ts-mockito'; +import { UserRepository } from 'src/Infrastructure/User/Repository/UserRepository'; +import { User } from 'src/Domain/User/User.entity'; +import { RemoveUserCommandHandler } from './RemoveUserCommandHandler'; +import { RemoveUserCommand } from './RemoveUserCommand'; +import { UserNotFoundException } from 'src/Domain/User/Exception/UserNotFoundException'; +import { CantRemoveYourselfException } from 'src/Domain/User/Exception/CantRemoveYourselfException'; + +describe('RemoveUserCommandHandler', () => { + let userRepository: UserRepository; + let removedUser: User; + let handler: RemoveUserCommandHandler; + + const command = new RemoveUserCommand( + '17efcbee-bd2f-410e-9e99-51684b592bad', + 'c47f70f1-101c-4d9b-84e1-f88bed74f957' + ); + + beforeEach(() => { + userRepository = mock(UserRepository); + removedUser = mock(User); + + handler = new RemoveUserCommandHandler( + instance(userRepository) + ); + }); + + it('testUserRemovedSuccessfully', async () => { + when(userRepository.findOneById('17efcbee-bd2f-410e-9e99-51684b592bad')) + .thenResolve(instance(removedUser)); + when(removedUser.getId()).thenReturn( + '17efcbee-bd2f-410e-9e99-51684b592bad' + ); + when( + userRepository.save(instance(removedUser)) + ).thenResolve(instance(removedUser)); + + await handler.execute(command); + + verify( + userRepository.remove(instance(removedUser)) + ).once(); + verify(userRepository.findOneById('17efcbee-bd2f-410e-9e99-51684b592bad')).once(); + }); + + it('testUserNotFound', async () => { + when(userRepository.findOneById('17efcbee-bd2f-410e-9e99-51684b592bad')) + .thenResolve(null); + + try { + expect(await handler.execute(command)).toBeUndefined(); + } catch (e) { + expect(e).toBeInstanceOf(UserNotFoundException); + expect(e.message).toBe('users.errors.not_found'); + verify(userRepository.findOneById('17efcbee-bd2f-410e-9e99-51684b592bad')).once(); + verify(userRepository.remove(anything())).never(); + } + }); + + it('testCantRemoveYourself', async () => { + const command2 = new RemoveUserCommand( + '17efcbee-bd2f-410e-9e99-51684b592bad', + '17efcbee-bd2f-410e-9e99-51684b592bad' + ); + + try { + expect(await handler.execute(command2)).toBeUndefined(); + } catch (e) { + expect(e).toBeInstanceOf(CantRemoveYourselfException); + expect(e.message).toBe('users.errors.cant_remove_yourself'); + verify(userRepository.findOneById(anything())).never(); + verify(userRepository.remove(anything())).never(); + } + }); +}); diff --git a/api/src/Application/User/Command/RemoveUserCommandHandler.ts b/api/src/Application/User/Command/RemoveUserCommandHandler.ts new file mode 100644 index 0000000..7229465 --- /dev/null +++ b/api/src/Application/User/Command/RemoveUserCommandHandler.ts @@ -0,0 +1,28 @@ +import { Inject } from '@nestjs/common'; +import { CommandHandler } from '@nestjs/cqrs'; +import { CantRemoveYourselfException } from 'src/Domain/User/Exception/CantRemoveYourselfException'; +import { UserNotFoundException } from 'src/Domain/User/Exception/UserNotFoundException'; +import { IUserRepository } from 'src/Domain/User/Repository/IUserRepository'; +import { RemoveUserCommand } from './RemoveUserCommand'; + +@CommandHandler(RemoveUserCommand) +export class RemoveUserCommandHandler { + constructor( + @Inject('IUserRepository') + private readonly userRepository: IUserRepository, + ) {} + + public async execute({ id, currentUserId }: RemoveUserCommand): Promise { + if (id === currentUserId) { + throw new CantRemoveYourselfException(); + } + + const user = await this.userRepository.findOneById(id); + + if (!user) { + throw new UserNotFoundException(); + } + + await this.userRepository.remove(user); + } +} diff --git a/api/src/Domain/User/Exception/CantRemoveYourselfException.ts b/api/src/Domain/User/Exception/CantRemoveYourselfException.ts new file mode 100644 index 0000000..a62ad23 --- /dev/null +++ b/api/src/Domain/User/Exception/CantRemoveYourselfException.ts @@ -0,0 +1,5 @@ +export class CantRemoveYourselfException extends Error { + constructor() { + super('users.errors.cant_remove_yourself'); + } +} diff --git a/api/src/Domain/User/Repository/IUserRepository.ts b/api/src/Domain/User/Repository/IUserRepository.ts index 62cb833..36507ca 100644 --- a/api/src/Domain/User/Repository/IUserRepository.ts +++ b/api/src/Domain/User/Repository/IUserRepository.ts @@ -2,6 +2,7 @@ import { User, UserRole } from '../User.entity'; export interface IUserRepository { save(user: User): Promise; + remove(user: User): void; findOneByApiToken(apiToken: string): Promise; findOneByEmail(email: string): Promise; findOneById(id: string): Promise; diff --git a/api/src/Infrastructure/School/Action/User/GetSchoolUsersAction.ts b/api/src/Infrastructure/School/Action/User/GetSchoolUsersAction.ts index bdf4ada..110f342 100644 --- a/api/src/Infrastructure/School/Action/User/GetSchoolUsersAction.ts +++ b/api/src/Infrastructure/School/Action/User/GetSchoolUsersAction.ts @@ -6,8 +6,8 @@ import { IdDTO } from 'src/Infrastructure/Common/DTO/IdDTO'; import { Roles } from 'src/Infrastructure/User/Decorator/Roles'; import { RolesGuard } from 'src/Infrastructure/User/Security/RolesGuard'; import { UserRole } from 'src/Domain/User/User.entity'; -import { UserSchoolView } from 'src/Application/School/View/UserSchoolView'; import { GetSchoolUsersQuery } from 'src/Application/School/Query/User/GetSchoolUsersQuery'; +import { UserSummaryView } from 'src/Application/User/View/UserSummaryView'; @Controller('schools') @ApiTags('School') @@ -22,7 +22,7 @@ export class GetSchoolUsersAction { @Get(':id/users') @Roles(UserRole.PHOTOGRAPHER, UserRole.DIRECTOR) @ApiOperation({summary: 'Get users for a specific school'}) - public async index(@Param() dto: IdDTO): Promise { + public async index(@Param() dto: IdDTO): Promise { try { return await this.queryBus.execute(new GetSchoolUsersQuery(dto.id)); } catch (e) { diff --git a/api/src/Infrastructure/School/Repository/SchoolUserRepository.ts b/api/src/Infrastructure/School/Repository/SchoolUserRepository.ts index 0971ff4..52386c8 100644 --- a/api/src/Infrastructure/School/Repository/SchoolUserRepository.ts +++ b/api/src/Infrastructure/School/Repository/SchoolUserRepository.ts @@ -41,12 +41,14 @@ export class SchoolUserRepository implements ISchoolUserRepository { .createQueryBuilder('schoolUser') .select([ 'schoolUser.id', + 'user.id', 'user.firstName', 'user.lastName', 'user.email' ]) .innerJoin('schoolUser.user', 'user') .innerJoin('schoolUser.school', 'school', 'school.id = :schoolId', { schoolId }) + .orderBy('user.lastName', 'ASC') .getMany(); } } diff --git a/api/src/Infrastructure/User/Action/RemoveUserAction.ts b/api/src/Infrastructure/User/Action/RemoveUserAction.ts new file mode 100644 index 0000000..ce57879 --- /dev/null +++ b/api/src/Infrastructure/User/Action/RemoveUserAction.ts @@ -0,0 +1,43 @@ +import { + Controller, + Inject, + BadRequestException, + UseGuards, + Param, + Delete +} from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { ICommandBus } from 'src/Application/ICommandBus'; +import { RemoveUserCommand } from 'src/Application/User/Command/RemoveUserCommand'; +import { UserRole } from 'src/Domain/User/User.entity'; +import { IdDTO } from 'src/Infrastructure/Common/DTO/IdDTO'; +import { Roles } from 'src/Infrastructure/User/Decorator/Roles'; +import { RolesGuard } from 'src/Infrastructure/User/Security/RolesGuard'; +import { LoggedUser } from '../Decorator/LoggedUser'; +import { UserAuthView } from '../Security/UserAuthView'; + +@Controller('users') +@ApiTags('User') +@ApiBearerAuth() +@UseGuards(AuthGuard('bearer'), RolesGuard) +export class RemoveUserAction { + constructor( + @Inject('ICommandBus') + private readonly commandBus: ICommandBus + ) {} + + @Delete(':id') + @Roles(UserRole.PHOTOGRAPHER) + @ApiOperation({ summary: 'Remove user' }) + public async index( + @Param() { id }: IdDTO, + @LoggedUser() user: UserAuthView + ) { + try { + await this.commandBus.execute(new RemoveUserCommand(id, user.id)); + } catch (e) { + throw new BadRequestException(e.message); + } + } +} diff --git a/api/src/Infrastructure/User/Repository/UserRepository.ts b/api/src/Infrastructure/User/Repository/UserRepository.ts index 88b2d0f..25e281b 100644 --- a/api/src/Infrastructure/User/Repository/UserRepository.ts +++ b/api/src/Infrastructure/User/Repository/UserRepository.ts @@ -15,6 +15,10 @@ export class UserRepository implements IUserRepository { return this.repository.save(user); } + public remove(user: User): void { + this.repository.delete(user.getId()); + } + public findOneByApiToken(apiToken: string): Promise { return this.repository .createQueryBuilder('user') diff --git a/api/src/Infrastructure/User/user.module.ts b/api/src/Infrastructure/User/user.module.ts index f5eddf5..761ab63 100644 --- a/api/src/Infrastructure/User/user.module.ts +++ b/api/src/Infrastructure/User/user.module.ts @@ -18,6 +18,8 @@ import { CreateUserCommandHandler } from 'src/Application/User/Command/CreateUse import { CreateUserAction } from './Action/CreateUserAction'; import { GetUsersByRoleQueryHandler } from 'src/Application/User/Query/GetUsersByRoleQueryHandler'; import { GetPhotographersAction } from './Action/GetPhotographersAction'; +import { RemoveUserCommandHandler } from 'src/Application/User/Command/RemoveUserCommandHandler'; +import { RemoveUserAction } from './Action/RemoveUserAction'; @Module({ imports: [ @@ -29,6 +31,7 @@ import { GetPhotographersAction } from './Action/GetPhotographersAction'; UserLoginAction, UpdateMeAction, GetMeAction, + RemoveUserAction, CreateUserAction, GetPhotographersAction ], @@ -43,6 +46,7 @@ import { GetPhotographersAction } from './Action/GetPhotographersAction'; RolesGuard, CreateUserCommandHandler, GetUsersByRoleQueryHandler, + RemoveUserCommandHandler, ] }) export class UserModule {} diff --git a/client/i18n/fr.json b/client/i18n/fr.json index 229f0fb..deeee9d 100644 --- a/client/i18n/fr.json +++ b/client/i18n/fr.json @@ -5,6 +5,7 @@ "pagination": "Affichage {start}-{from} sur {totalItems}", "common": { "actions": "Actions", + "not_defined": "Non-défini", "form": { "save": "Enregistrer", "edit": "Modifier", @@ -30,6 +31,9 @@ "title": "Mon compte", "logout": "Se déconnecter" }, + "calendar": { + "breadcrumb": "Calendrier" + }, "leads": { "breadcrumb": "Prospects", "statutes": { @@ -58,7 +62,7 @@ "address": "Adresse", "reference": "Référence", "number_of_students": "Nombre d'élèves", - "informations": "Fiche prospect \"{name}\"", + "informations": "Fiche prospect", "type": "Type d'établissement", "status": "Statut" }, @@ -151,6 +155,9 @@ "title": "Edition de l'établissement" }, "users": { + "delete": { + "confirm": "Êtes-vous sûr de vouloir supprimer cet utilisateur ?" + }, "add": { "title": "Créer un compte utilisateur", "notice": "L'utilisateur que vous allez créer içi aura uniquement à cet établissement." diff --git a/client/src/components/Nav.svelte b/client/src/components/Nav.svelte index 7784354..bd6b7bd 100644 --- a/client/src/components/Nav.svelte +++ b/client/src/components/Nav.svelte @@ -6,6 +6,7 @@ import SchoolIcon from './icons/SchoolIcon.svelte'; import LeadIcon from './icons/LeadIcon.svelte'; import UsersIcon from './icons/UsersIcon.svelte'; + import CalendarIcon from './icons/CalendarIcon.svelte'; import ChevronDownIcon from './icons/ChevronDownIcon.svelte'; import { settings, currentPath } from 'store'; import { ROLE_PHOTOGRAPHER } from 'constants/roles'; @@ -14,6 +15,7 @@ const schoolsPath = '/admin/schools'; const productsPath = '/admin/products'; + const calendarPath = '/admin/calendar'; const usersPath = '/admin/users'; const leadsPath = '/admin/leads'; const activeClass = @@ -45,6 +47,17 @@ {$_('dashboard.title')} + {#if $session.user.scope === ROLE_PHOTOGRAPHER} +
  • + {#if $currentPath.includes(calendarPath)} + + {/if} + + + {$_('calendar.breadcrumb')} + +
  • + {/if}
  • {#if $currentPath.includes(schoolsPath)} diff --git a/client/src/components/icons/CalendarIcon.svelte b/client/src/components/icons/CalendarIcon.svelte new file mode 100644 index 0000000..7573328 --- /dev/null +++ b/client/src/components/icons/CalendarIcon.svelte @@ -0,0 +1,11 @@ + + + + + diff --git a/client/src/components/icons/ClassIcon.svelte b/client/src/components/icons/ClassIcon.svelte new file mode 100644 index 0000000..f3351cd --- /dev/null +++ b/client/src/components/icons/ClassIcon.svelte @@ -0,0 +1,7 @@ + + + + + diff --git a/client/src/routes/admin/leads/[id]/_Detail.svelte b/client/src/routes/admin/leads/[id]/_Detail.svelte index 630a3e0..399d86c 100644 --- a/client/src/routes/admin/leads/[id]/_Detail.svelte +++ b/client/src/routes/admin/leads/[id]/_Detail.svelte @@ -8,11 +8,6 @@
    - - - - - diff --git a/client/src/routes/admin/leads/[id]/index.svelte b/client/src/routes/admin/leads/[id]/index.svelte index d18713f..79595df 100644 --- a/client/src/routes/admin/leads/[id]/index.svelte +++ b/client/src/routes/admin/leads/[id]/index.svelte @@ -38,10 +38,15 @@
    -
    -{#if lead} - -{/if} +
    + {#if lead} +

    + {$_('leads.dashboard.informations')} + +

    + + {/if} +
    diff --git a/client/src/routes/admin/schools/[id]/_CardClass.svelte b/client/src/routes/admin/schools/[id]/_CardClass.svelte index 1169bff..3b98fda 100644 --- a/client/src/routes/admin/schools/[id]/_CardClass.svelte +++ b/client/src/routes/admin/schools/[id]/_CardClass.svelte @@ -1,14 +1,14 @@
    - +

    {$_('schools.dashboard.class')}

    -

    N/A

    +

    0

    diff --git a/client/src/routes/admin/schools/[id]/_CardOrder.svelte b/client/src/routes/admin/schools/[id]/_CardOrder.svelte index 8f7d413..64b600d 100644 --- a/client/src/routes/admin/schools/[id]/_CardOrder.svelte +++ b/client/src/routes/admin/schools/[id]/_CardOrder.svelte @@ -9,6 +9,6 @@

    {$_('schools.dashboard.orders')}

    -

    N/A

    +

    0

    diff --git a/client/src/routes/admin/schools/[id]/_CardPdv.svelte b/client/src/routes/admin/schools/[id]/_CardPdv.svelte new file mode 100644 index 0000000..def3427 --- /dev/null +++ b/client/src/routes/admin/schools/[id]/_CardPdv.svelte @@ -0,0 +1,28 @@ + + + diff --git a/client/src/routes/admin/schools/[id]/_CardProduct.svelte b/client/src/routes/admin/schools/[id]/_CardProduct.svelte index 209cda4..e2f6a5f 100644 --- a/client/src/routes/admin/schools/[id]/_CardProduct.svelte +++ b/client/src/routes/admin/schools/[id]/_CardProduct.svelte @@ -6,13 +6,13 @@ export let id; - let total = 'N/A'; + let total = 0; onMount(async () => { try { total = (await get(`schools/${id}/count-products`)).data.total; } catch (e) { - total = 'N/A'; + total = 0; } }); diff --git a/client/src/routes/admin/schools/[id]/_InformationSheet.svelte b/client/src/routes/admin/schools/[id]/_InformationSheet.svelte index 44a8bfb..290124c 100644 --- a/client/src/routes/admin/schools/[id]/_InformationSheet.svelte +++ b/client/src/routes/admin/schools/[id]/_InformationSheet.svelte @@ -1,7 +1,5 @@ @@ -49,12 +47,6 @@
    {/if} - {#if school.pdv} - - - - - {/if} {#if school.observation} diff --git a/client/src/routes/admin/schools/[id]/_SchoolUsers.svelte b/client/src/routes/admin/schools/[id]/_SchoolUsers.svelte index 23f2301..42d108c 100644 --- a/client/src/routes/admin/schools/[id]/_SchoolUsers.svelte +++ b/client/src/routes/admin/schools/[id]/_SchoolUsers.svelte @@ -1,28 +1,44 @@ +
    {$_('leads.dashboard.informations', { values: { name: lead.name }})}
    {$_('leads.dashboard.reference')}{school.numberOfStudents}
    {$_('schools.dashboard.pdv')}{format(new Date(school.pdv), 'dd/MM/yyyy HH:mm')}
    {$_('schools.dashboard.observation')}
    - {#each schoolUsers as schoolUser} + {#each schoolUsers as { firstName, lastName, email, id } (id)} - - - + + + {/each} diff --git a/client/src/routes/admin/schools/[id]/index.svelte b/client/src/routes/admin/schools/[id]/index.svelte index fadeb31..cea46e0 100644 --- a/client/src/routes/admin/schools/[id]/index.svelte +++ b/client/src/routes/admin/schools/[id]/index.svelte @@ -13,6 +13,7 @@ import { errorNormalizer } from 'normalizer/errors'; import ServerErrors from 'components/ServerErrors.svelte'; import H4Title from 'components/H4Title.svelte'; + import CardPdv from './_CardPdv.svelte'; import CardClass from './_CardClass.svelte'; import CardProduct from './_CardProduct.svelte'; import CardOrder from './_CardOrder.svelte'; @@ -52,12 +53,13 @@ {#if school} -
    +
    +
    -
    +

    {$_('schools.dashboard.informations')}

    {schoolUser.firstName} {schoolUser.lastName}{schoolUser.email}{firstName} {lastName}{email} + handleDelete(id)} confirmMessage={"schools.users.delete.confirm"} /> +