refacto : refactoring des classes facades

This commit is contained in:
styve Lioumba
2025-12-21 09:14:39 +01:00
parent a4fb20dd68
commit dfda229a03
52 changed files with 543 additions and 531 deletions

View File

@@ -18,7 +18,8 @@
"builder": "@angular-devkit/build-angular:application",
"options": {
"allowedCommonJsDependencies": [
"pocketbase"
"pocketbase",
"leaflet"
],
"outputPath": "dist/",
"index": "src/index.html",

View File

@@ -1,6 +1,6 @@
{
"name": "trouve-ton-profile",
"version": "1.3.0",
"version": "1.3.1",
"scripts": {
"ng": "ng",
"start": "bash replace-prod-env.sh src/environments/environment.development.ts $ENV_URL && ng serve",

View File

@@ -8,6 +8,8 @@ import { ProfileRepository } from '@app/domain/profiles/profile.repository';
import { SettingRepository } from '@app/domain/settings/setting.repository';
import { mockSettingRepo } from '@app/testing/setting.mock';
import { SETTING_REPOSITORY_TOKEN } from '@app/infrastructure/settings/setting-repository.token';
import { ToastrService } from 'ngx-toastr';
import { mockToastR } from '@app/testing/toastr.mock';
describe('AppComponent', () => {
let component: AppComponent;
@@ -41,6 +43,7 @@ describe('AppComponent', () => {
imports: [AppComponent],
providers: [
provideRouter([]),
{ provide: ToastrService, useValue: mockToastR },
{ provide: AUTH_REPOSITORY_TOKEN, useValue: mockAuthRepository },
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepo },
{ provide: SETTING_REPOSITORY_TOKEN, useValue: mockSettingRepository },

View File

@@ -348,7 +348,7 @@
d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z"
/>
</svg>
Localisation
<span class="hidden sm:inline">Localisation</span>
</button>
<button

View File

@@ -95,9 +95,7 @@
<img
alt="{{ user().name }}"
class="object-cover w-full h-full"
src="{{ environment.baseUrl }}/api/files/users/{{ user().id }}/{{
user().avatar
}}"
src="{{ profile().avatarUrl }}"
loading="lazy"
/>
} @else {

View File

@@ -8,6 +8,8 @@ import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repo
import { SectorRepository } from '@app/domain/sectors/sector.repository';
import { mockProfileRepo } from '@app/testing/profile.mock';
import { mockSectorRepo } from '@app/testing/sector.mock';
import { ToastrService } from 'ngx-toastr';
import { mockToastR } from '@app/testing/toastr.mock';
describe('ProfileListComponent', () => {
let component: ProfileListComponent;
@@ -22,6 +24,7 @@ describe('ProfileListComponent', () => {
provideRouter([]),
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepository },
{ provide: SECTOR_REPOSITORY_TOKEN, useValue: mockSectorRepository },
{ provide: ToastrService, useValue: mockToastR },
],
}).compileComponents();

View File

@@ -8,7 +8,6 @@ import { Router } from '@angular/router';
import { SearchFilters } from '@app/domain/search/search-filters';
import { FilterComponent } from '@app/shared/features/filter/filter.component';
import { PaginationComponent } from '@app/shared/features/pagination/pagination.component';
import { ProfileViewModel } from '@app/ui/profiles/profile.presenter.model';
import { ProfileListMapComponent } from '@app/shared/features/profile-list-map/profile-list-map.component';
type ViewMode = 'list' | 'map';
@@ -52,18 +51,11 @@ export class ProfileListComponent {
this.facade.load(filters);
}
// Nouvelles méthodes
switchToListView(): void {
this.viewMode.set('list');
}
switchToMapView(): void {
this.viewMode.set('map');
// Recharger les profils avec localisation si nécessaire
}
onProfileClickFromMap(profile: ProfileViewModel): void {
// Navigation vers le profil
this.router.navigate(['/profiles', profile.id]);
}
}

View File

@@ -5,6 +5,8 @@ import { provideRouter } from '@angular/router';
import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repository.token';
import { SectorRepository } from '@app/domain/sectors/sector.repository';
import { mockSectorRepo } from '@app/testing/sector.mock';
import { ToastrService } from 'ngx-toastr';
import { mockToastR } from '@app/testing/toastr.mock';
describe('ChipsComponent', () => {
let component: ChipsComponent;
@@ -18,6 +20,7 @@ describe('ChipsComponent', () => {
providers: [
provideRouter([]),
{ provide: SECTOR_REPOSITORY_TOKEN, useValue: mockSectorRepository },
{ provide: ToastrService, useValue: mockToastR },
],
}).compileComponents();

View File

@@ -1,4 +1,4 @@
import { Component, inject, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Component, inject, input, OnChanges, SimpleChanges } from '@angular/core';
import { TitleCasePipe } from '@angular/common';
import { UntilDestroy } from '@ngneat/until-destroy';
import { SectorFacade } from '@app/ui/sectors/sector.facade';
@@ -13,7 +13,7 @@ import { SectorFacade } from '@app/ui/sectors/sector.facade';
})
@UntilDestroy()
export class ChipsComponent implements OnChanges {
@Input({ required: true }) sectorId: string | null = null;
sectorId = input.required<string>();
private readonly sectorFacade = inject(SectorFacade);
protected sector = this.sectorFacade.sector;
@@ -21,8 +21,8 @@ export class ChipsComponent implements OnChanges {
protected readonly error = this.sectorFacade.error;
ngOnChanges(changes: SimpleChanges) {
if (this.sectorId && !this.loading().isLoading) {
this.sectorFacade.loadOne(this.sectorId);
if (this.sectorId() && !this.loading().isLoading) {
this.sectorFacade.loadOne(this.sectorId());
}
}
}

View File

@@ -1,12 +1,12 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyProfileProjectItemComponent } from './my-profile-project-item.component';
import { of } from 'rxjs';
import { Project } from '@app/domain/projects/project.model';
import { ProjectRepository } from '@app/domain/projects/project.repository';
import { provideRouter } from '@angular/router';
import { PROJECT_REPOSITORY_TOKEN } from '@app/infrastructure/projects/project-repository.token';
import { mockProjectRepo } from '@app/testing/project.mock';
import { ToastrService } from 'ngx-toastr';
import { mockToastR } from '@app/testing/toastr.mock';
describe('MyProfileProjectItemComponent', () => {
let component: MyProfileProjectItemComponent;
@@ -20,11 +20,15 @@ describe('MyProfileProjectItemComponent', () => {
providers: [
provideRouter([]),
{ provide: PROJECT_REPOSITORY_TOKEN, useValue: mockProjectRepository },
{ provide: ToastrService, useValue: mockToastR },
],
}).compileComponents();
fixture = TestBed.createComponent(MyProfileProjectItemComponent);
component = fixture.componentInstance;
fixture.componentRef.setInput('projectId', 'fakeId');
fixture.detectChanges();
await fixture.whenStable();

View File

@@ -1,4 +1,4 @@
import { Component, Input, OnInit } from '@angular/core';
import { Component, input, OnInit } from '@angular/core';
import { environment } from '@env/environment';
import { RouterLink } from '@angular/router';
import { ProjectFacade } from '@app/ui/projects/project.facade';
@@ -12,12 +12,12 @@ import { ProjectFacade } from '@app/ui/projects/project.facade';
})
export class MyProfileProjectItemComponent implements OnInit {
protected readonly environment = environment;
@Input({ required: true }) projectId = '';
projectId = input.required<string>();
private readonly projectFacade = new ProjectFacade();
protected project = this.projectFacade.project;
ngOnInit(): void {
this.projectFacade.loadOne(this.projectId);
this.projectFacade.loadOne(this.projectId());
}
}

View File

@@ -5,6 +5,8 @@ import { ProjectRepository } from '@app/domain/projects/project.repository';
import { provideRouter } from '@angular/router';
import { PROJECT_REPOSITORY_TOKEN } from '@app/infrastructure/projects/project-repository.token';
import { mockProfileRepo } from '@app/testing/profile.mock';
import { ToastrService } from 'ngx-toastr';
import { mockToastR } from '@app/testing/toastr.mock';
describe('MyProfileProjectListComponent', () => {
let component: MyProfileProjectListComponent;
@@ -17,6 +19,7 @@ describe('MyProfileProjectListComponent', () => {
providers: [
provideRouter([]),
{ provide: PROJECT_REPOSITORY_TOKEN, useValue: mockProjectRepository },
{ provide: ToastrService, useValue: mockToastR },
],
}).compileComponents();

View File

@@ -1,4 +1,4 @@
import { Component, Input, OnInit, signal } from '@angular/core';
import { Component, input, OnInit, signal } from '@angular/core';
import { PaginatorModule } from 'primeng/paginator';
import { ReactiveFormsModule } from '@angular/forms';
import { UntilDestroy } from '@ngneat/until-destroy';
@@ -14,8 +14,8 @@ import { ProjectFacade } from '@app/ui/projects/project.facade';
})
@UntilDestroy()
export class MyProfileProjectListComponent implements OnInit {
@Input({ required: true }) projectIds: string[] = [];
@Input({ required: true }) userId = '';
projectIds = input.required<string[]>();
userId = input<string>('');
protected projectIdSelected = signal<string | null>(null);
@@ -23,7 +23,7 @@ export class MyProfileProjectListComponent implements OnInit {
protected projects = this.projectFacade.projects;
ngOnInit(): void {
this.projectFacade.load(this.userId);
this.projectFacade.load(this.userId());
}
onProjectFormSubmitted($event: string | null) {

View File

@@ -27,6 +27,7 @@ describe('MyProfileUpdateCvFormComponent', () => {
fixture = TestBed.createComponent(MyProfileUpdateCvFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
await fixture.whenStable();

View File

@@ -1,6 +1,5 @@
import { Component, effect, inject, Input } from '@angular/core';
import { Component, effect, input } from '@angular/core';
import { NgClass } from '@angular/common';
import { ToastrService } from 'ngx-toastr';
import { ProfileViewModel } from '@app/ui/profiles/profile.presenter.model';
import { ProfileFacade } from '@app/ui/profiles/profile.facade';
import { ActionType } from '@app/domain/action-type.util';
@@ -14,9 +13,7 @@ import { Profile } from '@app/domain/profiles/profile.model';
styleUrl: './my-profile-update-cv-form.component.scss',
})
export class MyProfileUpdateCvFormComponent {
@Input({ required: true }) profile: ProfileViewModel | undefined = undefined;
private readonly toastrService = inject(ToastrService);
profile = input.required<ProfileViewModel>();
file: File | null = null; // Variable to store file
@@ -29,13 +26,6 @@ export class MyProfileUpdateCvFormComponent {
switch (this.loading().action) {
case ActionType.UPDATE:
if (!this.loading() && !this.error().hasError) {
//this.authService.updateUser();
this.toastrService.success(` Votre CV a bien été modifier !`, `Mise à jour`, {
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true,
});
}
}
});
@@ -45,7 +35,7 @@ export class MyProfileUpdateCvFormComponent {
if (this.file != null) {
const formData = new FormData();
formData.append('cv', this.file); // "avatar" est le nom du champ dans PocketBase
this.profileFacade.update(this.profile?.id!, formData as Partial<Profile>);
this.profileFacade.update(this.profile()?.id!, formData as Partial<Profile>);
}
}

View File

@@ -8,7 +8,6 @@ import {
} from '@angular/forms';
import { UntilDestroy } from '@ngneat/until-destroy';
import { MyProfileUpdateCvFormComponent } from '@app/shared/components/my-profile-update-cv-form/my-profile-update-cv-form.component';
import { ToastrService } from 'ngx-toastr';
import { ProfileViewModel } from '@app/ui/profiles/profile.presenter.model';
import { ProfileFacade } from '@app/ui/profiles/profile.facade';
import { ActionType } from '@app/domain/action-type.util';
@@ -32,7 +31,6 @@ import { LoadingComponent } from '@app/shared/components/loading/loading.compone
})
@UntilDestroy()
export class MyProfileUpdateFormComponent implements OnInit {
private readonly toastrService = inject(ToastrService);
protected readonly ActionType = ActionType;
@Input({ required: true }) profile: ProfileViewModel = {} as ProfileViewModel;
@@ -50,14 +48,10 @@ export class MyProfileUpdateFormComponent implements OnInit {
protected readonly sectorError = this.sectorFacade.error;
constructor() {
let message = '';
effect(() => {
if (!this.loading().isLoading) {
switch (this.loading().action) {
case ActionType.UPDATE:
message = `Vos informations personnelles ont bien été modifier !`;
this.customToast(ActionType.UPDATE, message);
break;
}
}
@@ -127,29 +121,4 @@ export class MyProfileUpdateFormComponent implements OnInit {
this.profileFacade.update(this.profile.id, data);
}
private customToast(action: ActionType, message: string): void {
if (this.error().hasError) {
this.toastrService.error(
`Une erreur s'est produite, veuillez réessayer ulterieurement`,
`Erreur`,
{
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true,
}
);
return;
}
this.toastrService.success(
`${message}`,
`${action === ActionType.UPDATE ? 'Mise à jour' : ''}`,
{
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true,
}
);
}
}

View File

@@ -1,4 +1,4 @@
@if (projectId) {
@if (projectId()) {
<div class="space-y-6">
<!-- Section Image du projet -->
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 animate-fade-in">
@@ -22,7 +22,7 @@
<h3 class="text-xl font-bold text-gray-900 dark:text-white">Image du projet</h3>
</div>
@if (projectId === 'add'.toLowerCase()) {
@if (projectId() === 'add'.toLowerCase()) {
<app-project-picture-form [project]="undefined" />
} @else {
<app-project-picture-form [project]="project()" />

View File

@@ -5,8 +5,6 @@ import { ToastrService } from 'ngx-toastr';
import { provideRouter } from '@angular/router';
import { PROJECT_REPOSITORY_TOKEN } from '@app/infrastructure/projects/project-repository.token';
import { ProjectRepository } from '@app/domain/projects/project.repository';
import { of } from 'rxjs';
import { Project } from '@app/domain/projects/project.model';
import { AUTH_REPOSITORY_TOKEN } from '@app/infrastructure/authentification/auth-repository.token';
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
import { AuthRepository } from '@app/domain/authentification/auth.repository';
@@ -40,6 +38,8 @@ describe('MyProfileUpdateProjectFormComponent', () => {
fixture = TestBed.createComponent(MyProfileUpdateProjectFormComponent);
component = fixture.componentInstance;
fixture.componentRef.setInput('projectId', 'fakeId');
fixture.detectChanges();
});

View File

@@ -1,17 +1,7 @@
import {
Component,
effect,
inject,
Input,
OnChanges,
OnInit,
output,
SimpleChanges,
} from '@angular/core';
import { Component, effect, inject, input, output } from '@angular/core';
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { PaginatorModule } from 'primeng/paginator';
import { ProjectPictureFormComponent } from '@app/shared/components/project-picture-form/project-picture-form.component';
import { ToastrService } from 'ngx-toastr';
import { ProjectFacade } from '@app/ui/projects/project.facade';
import { CreateProjectDto } from '@app/domain/projects/dto/create-project.dto';
import { ActionType } from '@app/domain/action-type.util';
@@ -25,10 +15,8 @@ import { AuthFacade } from '@app/ui/authentification/auth.facade';
templateUrl: './my-profile-update-project-form.component.html',
styleUrl: './my-profile-update-project-form.component.scss',
})
export class MyProfileUpdateProjectFormComponent implements OnInit, OnChanges {
@Input({ required: true }) projectId: string | null = null;
private readonly toastrService = inject(ToastrService);
export class MyProfileUpdateProjectFormComponent {
projectId = input.required<string | null>();
private readonly projectFacade = new ProjectFacade();
protected readonly ActionType = ActionType;
@@ -50,17 +38,12 @@ export class MyProfileUpdateProjectFormComponent implements OnInit, OnChanges {
formIsUpdated = output<string | null>();
constructor() {
let message = '';
effect(() => {
if (!this.loading().isLoading) {
switch (this.loading().action) {
case ActionType.CREATE:
message = `Le projet ${this.projectForm.getRawValue().nom} a bien été créer !`;
this.customToast(ActionType.CREATE, message);
break;
case ActionType.UPDATE:
message = `Les informations du projet ${this.projectForm.getRawValue().nom} ont bien été modifier !`;
this.customToast(ActionType.UPDATE, message);
break;
}
@@ -73,20 +56,26 @@ export class MyProfileUpdateProjectFormComponent implements OnInit, OnChanges {
}
}
});
}
ngOnInit(): void {
if (this.projectId == 'add'.toLowerCase()) {
this.projectForm.setValue({
nom: '',
description: '',
lien: '',
});
}
if (this.projectId != null && this.projectId != 'add'.toLowerCase()) {
this.projectFacade.loadOne(this.projectId);
}
effect(
() => {
const id = this.projectId();
if (id) {
if (id.toLowerCase() === 'add') {
// Mode Création : On vide le formulaire
this.projectForm.reset({
nom: '',
description: '',
lien: '',
});
} else {
// Mode Édition : On charge les données
this.projectFacade.loadOne(id);
}
}
},
{ allowSignalWrites: true }
);
}
onSubmit() {
@@ -94,7 +83,7 @@ export class MyProfileUpdateProjectFormComponent implements OnInit, OnChanges {
return;
}
if (this.projectId != null && this.projectId != 'add'.toLowerCase()) {
if (this.projectId() != null && this.projectId() != 'add'.toLowerCase()) {
// Update
this.projectFacade.update(this.project()!.id, this.projectForm.getRawValue());
} else {
@@ -107,35 +96,4 @@ export class MyProfileUpdateProjectFormComponent implements OnInit, OnChanges {
this.projectFacade.create(projectDto);
}
}
ngOnChanges(changes: SimpleChanges): void {
this.projectId = changes['projectId'].currentValue;
this.ngOnInit();
}
private customToast(action: ActionType, message: string): void {
if (this.error().hasError) {
this.toastrService.error(
`Une erreur s'est produite, veuillez réessayer ulterieurement`,
`Erreur`,
{
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true,
}
);
return;
}
this.formIsUpdated.emit(this.project()!.id);
this.toastrService.success(
`${message}`,
`${action === ActionType.UPDATE ? 'Mise à jour' : 'Nouveau projet'}`,
{
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true,
}
);
}
}

View File

@@ -11,6 +11,8 @@ import { ProfileRepository } from '@app/domain/profiles/profile.repository';
import { mockProfileRepo } from '@app/testing/profile.mock';
import { mockAuthRepo } from '@app/testing/auth.mock';
import { mockThemeService } from '@app/testing/theme.mock';
import { ToastrService } from 'ngx-toastr';
import { mockToastR } from '@app/testing/toastr.mock';
describe('NavBarComponent', () => {
let component: NavBarComponent;
@@ -38,6 +40,7 @@ describe('NavBarComponent', () => {
providers: [
provideRouter([]),
{ provide: ThemeService, useValue: mockTheme },
{ provide: ToastrService, useValue: mockToastR },
{ provide: AUTH_REPOSITORY_TOKEN, useValue: mockAuthRepository },
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepository },
],

View File

@@ -5,6 +5,8 @@ import { ProjectRepository } from '@app/domain/projects/project.repository';
import { provideRouter } from '@angular/router';
import { PROJECT_REPOSITORY_TOKEN } from '@app/infrastructure/projects/project-repository.token';
import { mockProjectRepo } from '@app/testing/project.mock';
import { ToastrService } from 'ngx-toastr';
import { mockToastR } from '@app/testing/toastr.mock';
describe('ProjectListComponent', () => {
let component: ProjectListComponent;
@@ -18,6 +20,7 @@ describe('ProjectListComponent', () => {
providers: [
provideRouter([]),
{ provide: PROJECT_REPOSITORY_TOKEN, useValue: mockProjectRepository },
{ provide: ToastrService, useValue: mockToastR },
],
}).compileComponents();

View File

@@ -1,4 +1,4 @@
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Component, input, OnChanges, SimpleChanges } from '@angular/core';
import { UntilDestroy } from '@ngneat/until-destroy';
import { ProjectItemComponent } from '@app/shared/components/project-item/project-item.component';
import { ProjectFacade } from '@app/ui/projects/project.facade';
@@ -12,13 +12,13 @@ import { ProjectFacade } from '@app/ui/projects/project.facade';
})
@UntilDestroy()
export class ProjectListComponent implements OnChanges {
@Input({ required: true }) userProjectId = '';
userProjectId = input.required<string>({});
private readonly projectFacade = new ProjectFacade();
protected projects = this.projectFacade.projects;
ngOnChanges(changes: SimpleChanges) {
this.projectFacade.load(this.userProjectId);
this.projectFacade.load(this.userProjectId());
}
}

View File

@@ -1,7 +1,6 @@
import { Component, effect, inject, Input, output, signal } from '@angular/core';
import { Component, effect, Input, output, signal } from '@angular/core';
import { NgClass, NgTemplateOutlet } from '@angular/common';
import { environment } from '@env/environment';
import { ToastrService } from 'ngx-toastr';
import { ProjectViewModel } from '@app/ui/projects/project.presenter.model';
import { ProjectFacade } from '@app/ui/projects/project.facade';
import { ActionType } from '@app/domain/action-type.util';
@@ -20,7 +19,6 @@ export class ProjectPictureFormComponent {
protected readonly ActionType = ActionType;
onFormSubmitted = output<any>();
private readonly toastrService = inject(ToastrService);
private readonly projectFacade = new ProjectFacade();
protected readonly loading = this.projectFacade.loading;
@@ -36,8 +34,6 @@ export class ProjectPictureFormComponent {
if (!this.loading().isLoading && this.onSubmitted()) {
switch (this.loading().action) {
case ActionType.UPDATE:
message = `L'aperçu du projet ${this.project!.nom} ont bien été modifier !`;
this.customToast(ActionType.UPDATE, message);
this.onSubmitted.set(false);
this.onFormSubmitted.emit('');
break;
@@ -71,29 +67,4 @@ export class ProjectPictureFormComponent {
};
reader.readAsDataURL(file);
}
private customToast(action: ActionType, message: string): void {
if (this.error().hasError) {
this.toastrService.error(
`Une erreur s'est produite, veuillez réessayer ulterieurement`,
`Erreur`,
{
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true,
}
);
return;
}
this.toastrService.success(
`${message}`,
`${action === ActionType.UPDATE ? 'Mise à jour' : ''}`,
{
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true,
}
);
}
}

View File

@@ -40,7 +40,6 @@ export class UserAvatarFormComponent {
if (!this.loading().isLoading) {
switch (this.loading().action) {
case ActionType.UPDATE:
message = `Votre photo de profile a bien été modifier !`;
if (this.onSubmitted()) {
this.customToast(ActionType.UPDATE, message);
}

View File

@@ -1,11 +1,15 @@
@if (user() !== undefined) {
<a [routerLink]="[slug()]" [state]="{ user: user(), profile }" class="block group">
<a
[routerLink]="[profile()!.slug!]"
[state]="{ user: user(), profile: profile() }"
class="block group"
>
<!-- Card du profil -->
<div
class="relative bg-white dark:bg-gray-800 rounded-xl shadow-lg overflow-hidden transition-all duration-300 hover:shadow-2xl hover:-translate-y-2"
>
<!-- Badge vérifié (position absolue en haut à droite) -->
@if (profile.estVerifier) {
@if (profile().estVerifier) {
<div class="absolute top-3 right-3 z-10 animate-pulse-slow">
<div class="bg-purple-500/20 backdrop-blur-md p-2 rounded-full">
<svg
@@ -29,10 +33,10 @@
<!-- Avatar avec bordure gradient -->
<div class="relative inline-block mb-4">
<div class="w-32 h-32 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 p-1">
@if (user().avatar) {
@if (profile().avatarUrl) {
<img
class="w-full h-full rounded-full object-cover grayscale group-hover:grayscale-0 transition-all duration-500 group-hover:scale-105"
src="{{ environment.baseUrl }}/api/files/users/{{ user().id }}/{{ user().avatar }}"
src="{{ profile().avatarUrl }}"
alt="{{ user().username }}"
loading="lazy"
/>
@@ -61,12 +65,12 @@
{{ user().username }}
</h3>
} @else {
<h3 class="text-lg font-bold text-gray-500 dark:text-gray-400 mb-2">Non mention</h3>
<h3 class="text-lg font-bold text-gray-500 dark:text-gray-400 mb-2">Non renseig</h3>
}
<!-- Profession -->
<p class="text-sm font-semibold text-gray-600 dark:text-gray-300 mb-2">
{{ profile.profession || 'Profession non renseignée' }}
{{ profile().profession || 'Profession non renseignée' }}
</p>
</div>

View File

@@ -4,13 +4,13 @@ import { VerticalProfileItemComponent } from './vertical-profile-item.component'
import { UserRepository } from '@app/domain/users/user.repository';
import { provideRouter } from '@angular/router';
import { USER_REPOSITORY_TOKEN } from '@app/infrastructure/users/user-repository.token';
import { User } from '@app/domain/users/user.model';
import { of } from 'rxjs';
import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repository.token';
import { SectorRepository } from '@app/domain/sectors/sector.repository';
import { Sector } from '@app/domain/sectors/sector.model';
import { mockUserRepo } from '@app/testing/user.mock';
import { mockSectorRepo } from '@app/testing/sector.mock';
import { ProfileViewModel } from '@app/ui/profiles/profile.presenter.model';
import { ToastrService } from 'ngx-toastr';
import { mockToastR } from '@app/testing/toastr.mock';
describe('VerticalProfileItemComponent', () => {
let component: VerticalProfileItemComponent;
@@ -19,6 +19,11 @@ describe('VerticalProfileItemComponent', () => {
let mockUserRepository: jest.Mocked<Partial<UserRepository>> = mockUserRepo;
let mockSectorRepository: jest.Mocked<Partial<SectorRepository>> = mockSectorRepo;
const mockProfile: Partial<ProfileViewModel> = {
id: 'fakeId',
utilisateur: 'fakeUtilisateur',
};
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [VerticalProfileItemComponent],
@@ -26,11 +31,15 @@ describe('VerticalProfileItemComponent', () => {
provideRouter([]),
{ provide: USER_REPOSITORY_TOKEN, useValue: mockUserRepository },
{ provide: SECTOR_REPOSITORY_TOKEN, useValue: mockSectorRepository },
{ provide: ToastrService, useValue: mockToastR },
],
}).compileComponents();
fixture = TestBed.createComponent(VerticalProfileItemComponent);
component = fixture.componentInstance;
fixture.componentRef.setInput('profile', mockProfile);
fixture.detectChanges();
});

View File

@@ -1,4 +1,4 @@
import { Component, computed, inject, Input, OnInit } from '@angular/core';
import { Component, computed, inject, input, OnInit } from '@angular/core';
import { Router, RouterLink } from '@angular/router';
import { UntilDestroy } from '@ngneat/until-destroy';
import { environment } from '@env/environment';
@@ -15,9 +15,10 @@ import { UserFacade } from '@app/ui/users/user.facade';
})
@UntilDestroy()
export class VerticalProfileItemComponent implements OnInit {
@Input({ required: true }) profile: ProfileViewModel = {} as ProfileViewModel;
protected router = inject(Router);
profile = input.required<ProfileViewModel>();
protected readonly router = inject(Router);
private readonly facade = inject(UserFacade);
protected readonly environment = environment;
protected user = this.facade.user;
protected readonly loading = this.facade.loading;
@@ -25,13 +26,13 @@ export class VerticalProfileItemComponent implements OnInit {
protected slug = computed(() => {
const slug = this.user().slug ?? '';
const profileId = this.profile.id ? this.profile.id : '';
const profileId = this.profile().id ? this.profile().id : '';
return slug === '' ? profileId : slug.concat('-', profileId);
});
ngOnInit(): void {
this.facade.loadOne(this.profile.utilisateur);
if (this.profile()) {
this.facade.loadOne(this.profile().utilisateur);
}
}
protected readonly environment = environment;
}

View File

@@ -5,38 +5,40 @@
@for (profile of profiles; track profile.id) {
<app-vertical-profile-item [profile]="profile" />
} @empty {
<!-- Message si aucun profil -->
<div class="col-span-full">
<div
class="bg-gradient-to-r from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-700 rounded-xl p-12 text-center"
>
<div
class="inline-flex w-20 h-20 bg-white dark:bg-gray-600 rounded-full items-center justify-center mb-6 shadow-lg"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-10 h-10 text-gray-400 dark:text-gray-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
/>
</svg>
</div>
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-3">
Aucun profil trouvé
</h3>
<p class="text-gray-600 dark:text-gray-300 max-w-md mx-auto">
Aucun profil ne correspond à votre recherche. Essayez de modifier vos critères.
</p>
</div>
</div>
<ng-container *ngTemplateOutlet="emptyMessage" />
}
</div>
</div>
</section>
<ng-template #emptyMessage>
<!-- Message si aucun profil -->
<div class="col-span-full">
<div
class="bg-gradient-to-r from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-700 rounded-xl p-12 text-center"
>
<div
class="inline-flex w-20 h-20 bg-white dark:bg-gray-600 rounded-full items-center justify-center mb-6 shadow-lg"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-10 h-10 text-gray-400 dark:text-gray-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
/>
</svg>
</div>
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-3">Aucun profil trouvé</h3>
<p class="text-gray-600 dark:text-gray-300 max-w-md mx-auto">
Aucun profil ne correspond à votre recherche. Essayez de modifier vos critères.
</p>
</div>
</div>
</ng-template>

View File

@@ -1,11 +1,12 @@
import { Component, Input } from '@angular/core';
import { VerticalProfileItemComponent } from '@app/shared/components/vertical-profile-item/vertical-profile-item.component';
import { ProfileViewModel } from '@app/ui/profiles/profile.presenter.model';
import { NgTemplateOutlet } from '@angular/common';
@Component({
selector: 'app-vertical-profile-list',
standalone: true,
imports: [VerticalProfileItemComponent],
imports: [VerticalProfileItemComponent, NgTemplateOutlet],
templateUrl: './vertical-profile-list.component.html',
styleUrl: './vertical-profile-list.component.scss',
})

View File

@@ -5,12 +5,11 @@ import { SectorRepository } from '@app/domain/sectors/sector.repository';
import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repository.token';
import { provideRouter } from '@angular/router';
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
import { of } from 'rxjs';
import { Profile } from '@app/domain/profiles/profile.model';
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
import { Sector } from '@app/domain/sectors/sector.model';
import { mockProfileRepo } from '@app/testing/profile.mock';
import { mockSectorRepo } from '@app/testing/sector.mock';
import { ToastrService } from 'ngx-toastr';
import { mockToastR } from '@app/testing/toastr.mock';
describe('FilterComponent', () => {
let component: FilterComponent;
@@ -25,6 +24,7 @@ describe('FilterComponent', () => {
provideRouter([]),
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepository },
{ provide: SECTOR_REPOSITORY_TOKEN, useValue: mockSectorRepository },
{ provide: ToastrService, useValue: mockToastR },
],
}).compileComponents();

View File

@@ -8,6 +8,7 @@ import { AuthFacade } from '@app/ui/authentification/auth.facade';
import { ActivatedRoute, provideRouter, Router } from '@angular/router';
import { Subject } from 'rxjs';
// TODO : Model mock de la Facade
describe('ForgotPasswordComponent', () => {
let component: ForgotPasswordComponent;
let fixture: ComponentFixture<ForgotPasswordComponent>;
@@ -96,11 +97,11 @@ describe('ForgotPasswordComponent', () => {
fixture.detectChanges();
expect(mockToastrService.success).toHaveBeenCalledWith(
/*expect(mockToastrService.success).toHaveBeenCalledWith(
expect.stringContaining('success@test.com'),
expect.anything(),
expect.anything()
);
);*/
expect(mockRouter.navigate).toHaveBeenCalledWith(['/auth']);
});
@@ -112,7 +113,7 @@ describe('ForgotPasswordComponent', () => {
fixture.detectChanges();
expect(mockToastrService.error).toHaveBeenCalled();
//expect(mockToastrService.error).toHaveBeenCalled();
expect(mockRouter.navigate).not.toHaveBeenCalled();
expect(mockToastrService.success).not.toHaveBeenCalled();
});

View File

@@ -3,7 +3,6 @@ import { BtnLoadingComponent } from '@app/shared/components/btn-loading/btn-load
import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { Router, RouterLink } from '@angular/router';
import { AuthFacade } from '@app/ui/authentification/auth.facade';
import { ToastrService } from 'ngx-toastr';
import { ActionType } from '@app/domain/action-type.util';
@Component({
@@ -16,7 +15,6 @@ import { ActionType } from '@app/domain/action-type.util';
export class ForgotPasswordComponent {
private readonly fb = inject(FormBuilder);
private readonly facade = inject(AuthFacade);
private readonly toastrService = inject(ToastrService);
protected readonly router = inject(Router);
fpForm = this.fb.group({
@@ -33,8 +31,6 @@ export class ForgotPasswordComponent {
if (!this.loading().isLoading) {
switch (this.loading().action) {
case ActionType.CREATE:
message = `Un mail de réinitialisation vous a été envoyé à cette adresse mail : ${this.fpForm.getRawValue().email!}`;
this.customToast(ActionType.CREATE, message);
if (!this.error().hasError && this.facade.isRequestPasswordSent()) {
this.router.navigate(['/auth']);
}
@@ -51,30 +47,4 @@ export class ForgotPasswordComponent {
}
this.facade.sendRequestPasswordReset(this.fpForm.getRawValue().email!);
}
private customToast(action: ActionType, message: string): void {
if (this.error().hasError) {
this.toastrService.error(
`Une erreur s'est produite, veuillez réessayer ulterieurement`,
`Erreur`,
{
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true,
}
);
return;
}
this.toastrService.success(
`${message}`,
`${action === ActionType.CREATE ? 'Mise à jour' : ''}`,
{
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true,
disableTimeOut: true,
}
);
}
}

View File

@@ -3,7 +3,6 @@ import { Router, RouterLink } from '@angular/router';
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { LoginDto } from '@app/domain/authentification/dto/login-dto';
import { UntilDestroy } from '@ngneat/until-destroy';
import { ToastrService } from 'ngx-toastr';
import { ProgressBarModule } from 'primeng/progressbar';
import { AuthFacade } from '@app/ui/authentification/auth.facade';
import { ActionType } from '@app/domain/action-type.util';
@@ -19,7 +18,6 @@ import { BtnLoadingComponent } from '@app/shared/components/btn-loading/btn-load
})
@UntilDestroy()
export class LoginComponent {
private readonly toastrService = inject(ToastrService);
private readonly facade = inject(AuthFacade);
private formBuilder = inject(FormBuilder);
@@ -37,29 +35,19 @@ export class LoginComponent {
protected readonly error = this.facade.error;
constructor() {
let message = '';
effect(() => {
if (!this.loading().isLoading) {
switch (this.loading().action) {
case ActionType.READ:
if (!this.error().hasError) {
if (!this.authResponse()!.isValid && !this.authResponse()?.record.verified) {
message = `Vous ne pouvez pas vous connecter sans valider la verification envoyé à cet adresse ${this.authResponse()?.record.email!}`;
this.toastrService.warning(`${message}`, `CONNEXION`, {
closeButton: true,
progressBar: true,
disableTimeOut: true,
});
return;
}
this.router
.navigate(['/my-profile'], { state: { user: this.authResponse()!.record } })
.then(() => {
message = `Bienvenue parmi nous!`;
this.customToast(ActionType.READ, message);
});
if (this.error().hasError) {
this.loginForm.enable();
this.loginForm.markAllAsTouched();
this.loginForm.markAsDirty();
return;
}
this.router
.navigate(['/my-profile'], { state: { user: this.authResponse()!.record } })
.then(() => {});
break;
}
}
@@ -68,6 +56,8 @@ export class LoginComponent {
onSubmit() {
if (this.loginForm.invalid) {
this.loginForm.markAllAsTouched();
this.loginForm.markAsDirty();
return;
}
this.loginForm.disable();
@@ -78,35 +68,4 @@ export class LoginComponent {
this.facade.login(data);
}
private customToast(action: ActionType, message: string): void {
if (this.error().hasError) {
switch (this.error().action) {
case ActionType.READ:
this.toastrService.warning(`L'email ou mot de passe est incorrect`, `Erreur`, {
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true,
});
return;
default:
this.toastrService.error(
`Une erreur s'est produite, veuillez réessayer ulterieurement`,
`Erreur`,
{
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true,
}
);
return;
}
}
this.toastrService.success(`${message}`, `${action === ActionType.READ ? 'CONNEXION' : ''}`, {
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true,
});
}
}

View File

@@ -5,7 +5,6 @@ import { LocationFacade } from '@app/ui/location/location.facade';
import { ProfileFacade } from '@app/ui/profiles/profile.facade';
import { ProfileViewModel } from '@app/ui/profiles/profile.presenter.model';
import { ActionType } from '@app/domain/action-type.util';
import { FeedbackService } from '@app/ui/shared/services/feedback.service';
@Component({
selector: 'app-my-profile-map',
@@ -19,7 +18,6 @@ export class MyProfileMapComponent implements OnInit {
readonly profile = input<ProfileViewModel>();
private readonly locationFacade = inject(LocationFacade);
private readonly profileFacade = inject(ProfileFacade);
private readonly feedbackService = inject(FeedbackService);
private readonly loading = this.profileFacade.loading;
private readonly error = this.profileFacade.error;
@@ -53,11 +51,6 @@ export class MyProfileMapComponent implements OnInit {
switch (this.loading().action) {
case ActionType.UPDATE:
if (!this.error().hasError) {
this.feedbackService.notify(
ActionType.UPDATE,
'Vos coordonnées géographique ont été enregistrés.',
false
);
}
break;
}

View File

@@ -20,6 +20,6 @@ export class PdfViewerComponent {
return null;
}
return `${environment.baseUrl}/api/files/profiles/${currentProfile.id}/${currentProfile.cv}`;
return currentProfile.cv;
});
}

View File

@@ -3,7 +3,6 @@ import { Router, RouterLink } from '@angular/router';
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { RegisterDto } from '@app/domain/authentification/dto/register-dto';
import { UntilDestroy } from '@ngneat/until-destroy';
import { ToastrService } from 'ngx-toastr';
import { ProgressBarModule } from 'primeng/progressbar';
import { ActionType } from '@app/domain/action-type.util';
import { AuthFacade } from '@app/ui/authentification/auth.facade';
@@ -19,14 +18,12 @@ import { BtnLoadingComponent } from '@app/shared/components/btn-loading/btn-load
})
@UntilDestroy()
export class RegisterComponent {
private readonly toastrService = inject(ToastrService);
private readonly formBuilder = inject(FormBuilder);
private readonly router = inject(Router);
protected registerForm = this.formBuilder.group({
email: new FormControl('', [Validators.required, Validators.email]),
password: new FormControl('', Validators.required),
passwordConfirm: new FormControl('', Validators.required),
password: new FormControl('', [Validators.required, Validators.minLength(8)]),
passwordConfirm: new FormControl('', [Validators.required, Validators.minLength(8)]),
});
formSubmitted = output<any>();
@@ -37,7 +34,6 @@ export class RegisterComponent {
protected readonly isVerificationEmailSent = this.authFacade.isVerificationEmailSent;
constructor() {
let message = '';
effect(() => {
switch (this.authLoading().action) {
case ActionType.CREATE:
@@ -46,10 +42,11 @@ export class RegisterComponent {
!this.authError().hasError &&
this.isVerificationEmailSent()
) {
this.router.navigate(['/auth']).then(() => {
message = `Votre compte a bien été crée avec succès !\n Un mail vous a été envoyé à l'adresse ${this.registerForm.getRawValue().email!} pour confirmer votre inscription.`;
this.customToast(ActionType.CREATE, message);
});
if (this.authError().hasError) {
this.registerForm.enable();
return;
}
this.router.navigate(['/auth']).then(() => {});
}
break;
}
@@ -65,12 +62,6 @@ export class RegisterComponent {
this.registerForm.get('password')?.value !== this.registerForm.get('passwordConfirm')?.value
) {
this.registerForm.get('passwordConfirm')?.setErrors({ passwordMismatch: true });
this.toastrService.info(`Les mots de passe ne correspondent pas.`, `Information`, {
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true,
});
return;
}
@@ -81,30 +72,4 @@ export class RegisterComponent {
this.authFacade.register(data);
}
private customToast(action: ActionType, message: string): void {
if (this.authError().hasError) {
this.toastrService.error(
`Une erreur s'est produite, veuillez réessayer ulterieurement`,
`Erreur`,
{
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true,
}
);
return;
}
this.toastrService.success(
`${message}`,
`${action === ActionType.CREATE ? 'INSCRIPTION' : ''}`,
{
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true,
timeOut: 9000,
}
);
}
}

View File

@@ -10,6 +10,7 @@ import { mockSettingRepo } from '@app/testing/setting.mock';
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
import { mockProfileRepo } from '@app/testing/profile.mock';
import { ProfileViewModel } from '@app/ui/profiles/profile.presenter.model';
describe('SettingsComponent', () => {
let component: SettingsComponent;
@@ -32,6 +33,7 @@ describe('SettingsComponent', () => {
fixture = TestBed.createComponent(SettingsComponent);
component = fixture.componentInstance;
fixture.componentRef.setInput('profile', {} as ProfileViewModel);
fixture.detectChanges();
});

View File

@@ -2,7 +2,6 @@ import { Component, effect, inject, input, OnInit } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { ThemeType } from '@app/domain/settings/setting.model';
import { SettingsFacade } from '@app/ui/settings/settings.facade';
import { FeedbackService } from '@app/ui/shared/services/feedback.service';
import { ProfileViewModel } from '@app/ui/profiles/profile.presenter.model';
import { ProfileFacade } from '@app/ui/profiles/profile.facade';
import { ActionType } from '@app/domain/action-type.util';
@@ -19,11 +18,10 @@ export class SettingsComponent implements OnInit {
protected readonly ThemeType = ThemeType;
private readonly settingsFacade = inject(SettingsFacade);
private readonly fb: FormBuilder = inject(FormBuilder);
private readonly feedbackService = inject(FeedbackService);
private readonly profileFacade = inject(ProfileFacade);
private readonly loading = this.profileFacade.loading;
private readonly error = this.profileFacade.error;
profile = input<ProfileViewModel>();
profile = input.required<ProfileViewModel>();
settings = this.settingsFacade.settings;
@@ -53,11 +51,6 @@ export class SettingsComponent implements OnInit {
case ActionType.UPDATE:
if (!this.error().hasError && this.loading().isDone) {
this.updateForm(userSettings);
this.feedbackService.notify(
ActionType.UPDATE,
'Vos paramètres ont été enregistrés.',
false
);
}
break;
}

View File

@@ -0,0 +1,3 @@
export const mockFeedbackService = {
notify: jest.fn(),
};

View File

@@ -0,0 +1,9 @@
import { signal } from '@angular/core';
const initialFilters = { page: 1, perPage: 10, totalItems: 0, totalPages: 0, search: '' };
export const mockSearchService = {
getFilters: jest.fn(() => signal(initialFilters)),
setFilters: jest.fn(),
get: jest.fn(),
};

View File

@@ -5,6 +5,12 @@ import { FakeProfileRepository } from '@app/testing/domain/profiles/fake-profile
import { mockProfiles } from '@app/testing/profile.mock';
import { Profile } from '@app/domain/profiles/profile.model';
import { ProfileDTO } from '@app/domain/profiles/dto/create-profile.dto';
import { ToastrService } from 'ngx-toastr';
import { mockToastR } from '@app/testing/toastr.mock';
import { SearchService } from '@app/infrastructure/search/search.service';
import { mockSearchService } from '@app/testing/search.service.mock';
import { FeedbackService } from '@app/ui/shared/services/feedback.service';
import { mockFeedbackService } from '@app/testing/feedback.service.mock';
describe('ProfileFacade', () => {
let facade: ProfileFacade;
@@ -14,6 +20,9 @@ describe('ProfileFacade', () => {
providers: [
ProfileFacade,
{ provide: PROFILE_REPOSITORY_TOKEN, useClass: FakeProfileRepository },
{ provide: SearchService, useValue: mockSearchService },
{ provide: FeedbackService, useValue: mockFeedbackService },
{ provide: ToastrService, useValue: mockToastR },
],
});

View File

@@ -24,7 +24,7 @@ describe('ProfilePresenter', () => {
avatarUrl: '',
apropos: 'Développeur Angular & Node.js',
bio: 'Passionné de code.',
cv: 'cv.pdf',
cv: 'http://localhost:8090/api/files/profiles/1/cv.pdf',
isProfileVisible: true,
missingFields: [],
projets: ['p1', 'p2'],

View File

@@ -4,6 +4,8 @@ import { FakeProjectRepository } from '@app/testing/domain/projects/fake-project
import { PROJECT_REPOSITORY_TOKEN } from '@app/infrastructure/projects/project-repository.token';
import { fakeProjects } from '@app/testing/project.mock';
import { CreateProjectDto } from '@app/domain/projects/dto/create-project.dto';
import { ToastrService } from 'ngx-toastr';
import { mockToastR } from '@app/testing/toastr.mock';
describe('ProjectFacade', () => {
let facade: ProjectFacade;
@@ -13,6 +15,7 @@ describe('ProjectFacade', () => {
providers: [
ProjectFacade,
{ provide: PROJECT_REPOSITORY_TOKEN, useClass: FakeProjectRepository },
{ provide: ToastrService, useValue: mockToastR },
],
});

View File

@@ -3,6 +3,8 @@ import { SectorFacade } from '@app/ui/sectors/sector.facade';
import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repository.token';
import { FakeSectorRepository } from '@app/testing/domain/sectors/fake-sector.repository';
import { fakeSectors } from '@app/testing/sector.mock';
import { ToastrService } from 'ngx-toastr';
import { mockToastR } from '@app/testing/toastr.mock';
describe('SectorFacade', () => {
let sectorFacade: SectorFacade;
@@ -12,6 +14,7 @@ describe('SectorFacade', () => {
providers: [
SectorFacade,
{ provide: SECTOR_REPOSITORY_TOKEN, useClass: FakeSectorRepository },
{ provide: ToastrService, useValue: mockToastR },
],
});

View File

@@ -2,12 +2,18 @@ import { UserFacade } from '@app/ui/users/user.facade';
import { TestBed } from '@angular/core/testing';
import { USER_REPOSITORY_TOKEN } from '@app/infrastructure/users/user-repository.token';
import { FakeUserRepository } from '@app/testing/domain/users/fake-user.repository';
import { ToastrService } from 'ngx-toastr';
import { mockToastR } from '@app/testing/toastr.mock';
describe('UserFacade', () => {
let facade: UserFacade;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [UserFacade, { provide: USER_REPOSITORY_TOKEN, useClass: FakeUserRepository }],
providers: [
UserFacade,
{ provide: USER_REPOSITORY_TOKEN, useClass: FakeUserRepository },
{ provide: ToastrService, useValue: mockToastR },
],
});
facade = TestBed.inject(UserFacade);
});

View File

@@ -17,9 +17,12 @@ import { VerifyAuthenticatedUsecase } from '@app/usecase/authentification/verify
import { VerifyEmailUseCase } from '@app/usecase/authentification/verify-email.usecase';
import { GetCurrentUserUseCase } from '@app/usecase/authentification/get-current-user.usecase';
import { SendRequestPasswordResetUsecase } from '@app/usecase/authentification/send-request-password-reset.usecase';
import { FeedbackService } from '@app/ui/shared/services/feedback.service';
import { first } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class AuthFacade {
private readonly feedbackService = inject(FeedbackService);
private readonly authRepository = inject(AUTH_REPOSITORY_TOKEN);
private readonly profileFacade = new ProfileFacade();
@@ -55,32 +58,54 @@ export class AuthFacade {
login(loginDto: LoginDto) {
this.handleError(ActionType.READ, false, null, true);
let message = '';
this.loginUseCase.execute(loginDto).subscribe({
next: async (res: AuthResponse) => {
this.authResponse.set(res);
this.getCurrentUser();
this.handleError(ActionType.READ, false, null, false);
},
error: (err) => {
this.handleError(ActionType.READ, true, err.message, false);
},
});
this.loginUseCase
.execute(loginDto)
.pipe(first())
.subscribe({
next: async (res: AuthResponse) => {
this.authResponse.set(res);
this.getCurrentUser();
this.handleError(ActionType.READ, false, null, false);
if (!this.authResponse()!.isValid && !this.authResponse()?.record.verified) {
message = `Vous ne pouvez pas vous connecter sans valider la verification envoyé à cet adresse ${this.authResponse()?.record.email!}`;
this.feedbackService.notify(ActionType.READ, `${message}`);
return;
}
message = `Bienvenue parmi nous ${loginDto.email}!`;
this.feedbackService.notify(ActionType.READ, `${message}`);
},
error: (err) => {
this.handleError(ActionType.READ, true, err.message, false);
},
});
}
register(registerDto: RegisterDto) {
this.handleError(ActionType.CREATE, false, null, true);
this.registerUseCase.execute(registerDto).subscribe({
next: (user) => {
this.getCurrentUser();
this.sendVerificationEmail(registerDto.email);
this.createDefaultProfile(user.id);
this.handleError(ActionType.CREATE, false, null, false);
},
error: (err) => {
this.handleError(ActionType.CREATE, true, err.message, false);
},
});
let message = '';
this.registerUseCase
.execute(registerDto)
.pipe(first())
.subscribe({
next: (user) => {
this.getCurrentUser();
this.sendVerificationEmail(registerDto.email);
this.createDefaultProfile(user.id);
message = `Votre compte a bien été crée avec succès !\n Un mail vous a été envoyé à l'adresse ${registerDto.email} pour confirmer votre inscription.`;
this.feedbackService.notify(ActionType.CREATE, `${message}`);
this.handleError(ActionType.CREATE, false, null, false);
},
error: (err) => {
this.handleError(ActionType.CREATE, true, err.message, false);
},
});
}
logout() {
@@ -103,28 +128,38 @@ export class AuthFacade {
sendRequestPasswordReset(email: string) {
this.handleError(ActionType.CREATE, false, null, true);
this.senRequestPasswordResetUseCase.execute(email).subscribe({
next: (res) => {
this.isRequestPasswordSent.set(res);
this.handleError(ActionType.CREATE, false, null, false);
},
error: (err) => {
this.handleError(ActionType.CREATE, true, err.message, false);
},
});
let message = '';
this.senRequestPasswordResetUseCase
.execute(email)
.pipe(first())
.subscribe({
next: (res) => {
this.isRequestPasswordSent.set(res);
this.handleError(ActionType.CREATE, false, null, false);
message = `Un mail de réinitialisation vous a été envoyé à cette adresse mail : ${email}`;
this.feedbackService.notify(ActionType.CREATE, `${message}`);
},
error: (err) => {
this.handleError(ActionType.CREATE, true, err.message, false);
},
});
}
private sendVerificationEmail(email: string) {
this.handleError(ActionType.CREATE, false, null, true);
this.sendVerificationEmailUseCase.execute(email).subscribe({
next: (res) => {
this.isVerificationEmailSent.set(res);
this.handleError(ActionType.CREATE, false, null, false);
},
error: (err) => {
this.handleError(ActionType.CREATE, true, err.message, false);
},
});
this.sendVerificationEmailUseCase
.execute(email)
.pipe(first())
.subscribe({
next: (res) => {
this.isVerificationEmailSent.set(res);
this.handleError(ActionType.CREATE, false, null, false);
},
error: (err) => {
this.handleError(ActionType.CREATE, true, err.message, false);
},
});
}
private createDefaultProfile(userId: string) {
@@ -153,5 +188,8 @@ export class AuthFacade {
) {
this.error.set({ action, hasError, message });
this.loading.set({ action, isLoading });
if (hasError) {
this.feedbackService.notify(ActionType.READ, message!, true);
}
}
}

View File

@@ -19,12 +19,15 @@ import { SearchService } from '@app/infrastructure/search/search.service';
import { UpdateCoordinateProfileUseCase } from '@app/usecase/profiles/update-coordinate-profile.usecase';
import { SettingsProfileDto } from '@app/domain/profiles/dto/settings-profile.dto';
import { UpdateSettingsProfileUseCase } from '@app/usecase/profiles/update-settings-profile.usecase';
import { FeedbackService } from '@app/ui/shared/services/feedback.service';
import { first, Subscription } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class ProfileFacade {
private profileRepository = inject(PROFILE_REPOSITORY_TOKEN);
private readonly feedbackService = inject(FeedbackService);
private readonly profileRepository = inject(PROFILE_REPOSITORY_TOKEN);
private readonly searchService = inject(SearchService);
private readonly profilePresenter = new ProfilePresenter();
@@ -50,14 +53,21 @@ export class ProfileFacade {
message: null,
});
private searchSubscription: Subscription | null = null;
load(search?: SearchFilters) {
if (this.searchSubscription) {
this.searchSubscription.unsubscribe();
this.searchSubscription = null;
}
this.handleError(ActionType.READ, false, null, true);
if (search === undefined || search === null) {
search = this.searchFilters();
}
this.listUseCase.execute(search).subscribe({
this.searchSubscription = this.listUseCase.execute(search).subscribe({
next: (profilePaginated: ProfilePaginated) => {
const filters = {
...this.searchFilters(),
@@ -76,93 +86,124 @@ export class ProfileFacade {
this.profilePaginated.set(profileViewModelPaginated);
this.profiles.set(profileViewModelPaginated.items);
this.handleError(ActionType.READ, false, null, false);
this.searchSubscription = null;
},
error: (err) => {
this.handleError(ActionType.READ, false, err, false);
this.searchSubscription = null;
},
});
}
loadOne(profileId: string) {
this.handleError(ActionType.READ, false, null, true);
this.getUseCase.execute(profileId).subscribe({
next: (profile: Profile) => {
this.profile.set(this.profilePresenter.toViewModel(profile));
this.handleError(ActionType.READ, false, null, false);
},
error: (err) => {
this.handleError(ActionType.READ, false, err, false);
},
});
this.getUseCase
.execute(profileId)
.pipe(first())
.subscribe({
next: (profile: Profile) => {
this.profile.set(this.profilePresenter.toViewModel(profile));
this.handleError(ActionType.READ, false, null, false);
},
error: (err) => {
this.handleError(ActionType.READ, false, err, false);
},
});
}
loadOneByUserId(userId: string) {
this.handleError(ActionType.READ, false, null, true);
this.getUseCase.executeByUserId(userId).subscribe({
next: (profile: Profile) => {
this.profile.set(this.profilePresenter.toViewModel(profile));
this.handleError(ActionType.READ, false, null, false);
},
error: (err) => {
this.handleError(ActionType.READ, false, err, false);
},
});
this.getUseCase
.executeByUserId(userId)
.pipe(first())
.subscribe({
next: (profile: Profile) => {
this.profile.set(this.profilePresenter.toViewModel(profile));
this.handleError(ActionType.READ, false, null, false);
},
error: (err) => {
this.handleError(ActionType.READ, false, err, false);
},
});
}
create(profileDto: ProfileDTO) {
this.handleError(ActionType.CREATE, false, null, true);
let message = null;
this.createUseCase.execute(profileDto).subscribe({
next: (profile: Profile) => {
this.profile.set(this.profilePresenter.toViewModel(profile));
this.handleError(ActionType.CREATE, false, null, false);
},
error: (err) => {
this.handleError(ActionType.CREATE, false, err, false);
},
});
this.createUseCase
.execute(profileDto)
.pipe(first())
.subscribe({
next: (profile: Profile) => {
this.profile.set(this.profilePresenter.toViewModel(profile));
this.handleError(ActionType.CREATE, false, null, false);
},
error: (err) => {
this.handleError(ActionType.CREATE, false, err, false);
},
});
}
update(profileId: string, profile: Partial<Profile>) {
this.handleError(ActionType.UPDATE, false, null, true);
this.updateUseCase.execute(profileId, profile).subscribe({
next: (profile: Profile) => {
this.profile.set(this.profilePresenter.toViewModel(profile));
this.handleError(ActionType.UPDATE, false, null, false);
},
error: (err) => {
this.handleError(ActionType.UPDATE, false, err, false);
},
});
this.updateUseCase
.execute(profileId, profile)
.pipe(first())
.subscribe({
next: (profile: Profile) => {
this.profile.set(this.profilePresenter.toViewModel(profile));
this.handleError(ActionType.UPDATE, false, null, false);
const message = `Vos informations personnelles ont bien été modifier !`;
this.feedbackService.notify(ActionType.UPDATE, message);
},
error: (err) => {
this.handleError(ActionType.UPDATE, false, err, false);
},
});
}
updateCoordinate(profileId: string, latitude: number, longitude: number) {
this.handleError(ActionType.UPDATE, false, null, true);
this.updateCoordinateUseCase.execute(profileId, latitude, longitude).subscribe({
next: (profile: Profile) => {
this.profile.set(this.profilePresenter.toViewModel(profile));
this.handleError(ActionType.UPDATE, false, null, false);
},
error: (err) => {
this.handleError(ActionType.UPDATE, false, err, false);
},
});
this.updateCoordinateUseCase
.execute(profileId, latitude, longitude)
.pipe(first())
.subscribe({
next: (profile: Profile) => {
this.profile.set(this.profilePresenter.toViewModel(profile));
this.handleError(ActionType.UPDATE, false, null, false);
const message = `Vos coordonnées géographique ont été enregistrés.`;
this.feedbackService.notify(ActionType.UPDATE, message);
},
error: (err) => {
this.handleError(ActionType.UPDATE, false, err, false);
},
});
}
updateSettings(profileId: string, settings: SettingsProfileDto) {
this.handleError(ActionType.UPDATE, false, null, true);
this.updateSettingsUseCase.execute(profileId, settings).subscribe({
next: (profile: Profile) => {
this.profile.set(this.profilePresenter.toViewModel(profile));
this.handleError(ActionType.UPDATE, false, null, false, true);
},
error: (err) => {
this.handleError(ActionType.UPDATE, false, err, false);
},
});
this.updateSettingsUseCase
.execute(profileId, settings)
.pipe(first())
.subscribe({
next: (profile: Profile) => {
this.profile.set(this.profilePresenter.toViewModel(profile));
this.handleError(ActionType.UPDATE, false, null, false, true);
const message = `Vos paramètres ont été enregistrés.`;
this.feedbackService.notify(ActionType.UPDATE, message);
},
error: (err) => {
this.handleError(ActionType.UPDATE, false, err, false);
},
});
}
private handleError(
@@ -174,5 +215,8 @@ export class ProfileFacade {
) {
this.error.set({ action, hasError, message });
this.loading.set({ action, isLoading, isDone });
if (hasError) {
this.feedbackService.notify(ActionType.READ, message!, true);
}
}
}

View File

@@ -38,7 +38,7 @@ export class ProfilePresenter {
reseaux: profile.reseaux,
apropos: profile.apropos,
projets: profile.projets,
cv: profile.cv,
cv: `${environment.baseUrl}/api/files/profiles/${profile.id}/${profile.cv}`,
bio: profile.bio,
coordonnees: profile.coordonnees
? { latitude: profile!.coordonnees!.lat!, longitude: profile!.coordonnees!.lon! }
@@ -78,7 +78,9 @@ export class ProfilePresenter {
return false;
}
const hasProfession = !!currentProfile.profession;
const hasProfession = currentProfile.profession
? currentProfile.profession.toLowerCase() !== 'profession non renseignée'
: false;
const hasSector = !!currentProfile.secteur;
return hasProfession && hasSector;
@@ -87,7 +89,10 @@ export class ProfilePresenter {
private missingFields(currentProfile: Profile) {
const missing: string[] = [];
if (!currentProfile?.profession) {
if (
!currentProfile?.profession ||
currentProfile.profession.toLowerCase() === 'profession non renseignée'
) {
missing.push('profession');
}
if (!currentProfile?.secteur) {

View File

@@ -11,11 +11,14 @@ import { CreateProjectDto } from '@app/domain/projects/dto/create-project.dto';
import { ErrorResponse } from '@app/domain/error-response.util';
import { ActionType } from '@app/domain/action-type.util';
import { LoaderAction } from '@app/domain/loader-action.util';
import { first, Subscription } from 'rxjs';
import { FeedbackService } from '@app/ui/shared/services/feedback.service';
@Injectable({
providedIn: 'root',
})
export class ProjectFacade {
private readonly feedbackService = inject(FeedbackService);
private readonly projectRepo = inject(PROJECT_REPOSITORY_TOKEN);
private readonly createUseCase = new CreateProjectUseCase(this.projectRepo);
@@ -33,17 +36,26 @@ export class ProjectFacade {
});
private readonly projectPresenter = new ProjectPresenter();
private projectSubscription: Subscription | null = null;
load(userId: string) {
if (this.projectSubscription) {
this.projectSubscription.unsubscribe();
this.projectSubscription = null;
}
this.handleError(ActionType.READ, false, null, true);
this.listUseCase.execute(userId).subscribe({
this.projectSubscription = this.listUseCase.execute(userId).subscribe({
next: (projects: Project[]) => {
this.projects.set(this.projectPresenter.toViewModels(projects));
this.handleError(ActionType.READ, false, null, false);
this.projectSubscription = null;
},
error: (err) => {
this.handleError(ActionType.READ, false, err, false);
this.projectSubscription = null;
},
});
}
@@ -51,44 +63,58 @@ export class ProjectFacade {
loadOne(projectId: string) {
this.handleError(ActionType.READ, false, null, true);
this.getUseCase.execute(projectId).subscribe({
next: (project: Project) => {
this.project.set(this.projectPresenter.toViewModel(project));
this.handleError(ActionType.READ, false, null, false);
},
error: (err) => {
this.handleError(ActionType.READ, false, err, false);
},
});
this.getUseCase
.execute(projectId)
.pipe(first())
.subscribe({
next: (project: Project) => {
this.project.set(this.projectPresenter.toViewModel(project));
this.handleError(ActionType.READ, false, null, false);
},
error: (err) => {
this.handleError(ActionType.READ, false, err, false);
},
});
}
create(projectDto: CreateProjectDto) {
this.handleError(ActionType.CREATE, false, null, true);
this.createUseCase.execute(projectDto).subscribe({
next: (project: Project) => {
this.project.set(this.projectPresenter.toViewModel(project));
this.projects.update((prev) => [...prev, this.projectPresenter.toViewModel(project)]);
this.handleError(ActionType.CREATE, false, null, false);
},
error: (err) => {
this.handleError(ActionType.CREATE, false, err, false);
},
});
this.createUseCase
.execute(projectDto)
.pipe(first())
.subscribe({
next: (project: Project) => {
this.project.set(this.projectPresenter.toViewModel(project));
this.projects.update((prev) => [...prev, this.projectPresenter.toViewModel(project)]);
this.handleError(ActionType.CREATE, false, null, false);
const message = `Le projet ${project.nom} a bien été créer !`;
this.feedbackService.notify(ActionType.UPDATE, message);
},
error: (err) => {
this.handleError(ActionType.CREATE, false, err, false);
},
});
}
update(userId: string, data: any) {
this.handleError(ActionType.UPDATE, false, null, true);
this.UpdateUseCase.execute(userId, data).subscribe({
next: (project: Project) => {
this.project.set(this.projectPresenter.toViewModel(project));
this.handleError(ActionType.UPDATE, false, null, false);
},
error: (err) => {
this.handleError(ActionType.UPDATE, false, err, false);
},
});
this.UpdateUseCase.execute(userId, data)
.pipe(first())
.subscribe({
next: (project: Project) => {
this.project.set(this.projectPresenter.toViewModel(project));
this.handleError(ActionType.UPDATE, false, null, false);
const message = `Les informations du projet ${project.nom} ont bien été modifier !`;
this.feedbackService.notify(ActionType.UPDATE, message);
},
error: (err) => {
this.handleError(ActionType.UPDATE, false, err, false);
},
});
}
private handleError(
@@ -99,5 +125,9 @@ export class ProjectFacade {
) {
this.error.set({ action, hasError, message });
this.loading.set({ action, isLoading });
if (hasError) {
this.feedbackService.notify(ActionType.READ, message!, true);
}
}
}

View File

@@ -8,9 +8,12 @@ import { ErrorResponse } from '@app/domain/error-response.util';
import { SectorPresenterModel } from '@app/ui/sectors/sector.presenter.model';
import { Sector } from '@app/domain/sectors/sector.model';
import { SectorPresenter } from '@app/ui/sectors/sector.presenter';
import { first, Subscription } from 'rxjs';
import { FeedbackService } from '@app/ui/shared/services/feedback.service';
@Injectable()
export class SectorFacade {
private readonly feedbackService = inject(FeedbackService);
private readonly sectorRepo = inject(SECTOR_REPOSITORY_TOKEN);
private readonly listSectorUseCase = new ListSectorUsecase(this.sectorRepo);
@@ -27,31 +30,42 @@ export class SectorFacade {
});
private readonly sectorPresenter = new SectorPresenter();
private sectorSubscription: Subscription | null = null;
load() {
if (this.sectorSubscription) {
this.sectorSubscription.unsubscribe();
this.sectorSubscription = null;
}
this.handleError(ActionType.READ, false, null, true);
this.listSectorUseCase.execute().subscribe({
this.sectorSubscription = this.listSectorUseCase.execute().subscribe({
next: (sectors: Sector[]) => {
this.sectors.set(this.sectorPresenter.toViewModels(sectors));
this.handleError(ActionType.READ, false, null, false);
this.sectorSubscription = null;
},
error: (err) => {
this.handleError(ActionType.READ, false, err, false);
this.sectorSubscription = null;
},
});
}
loadOne(sectorId: string) {
this.handleError(ActionType.READ, false, null, true);
this.getSectorUseCase.execute(sectorId).subscribe({
next: (sector: Sector) => {
this.sector.set(this.sectorPresenter.toViewModel(sector));
this.handleError(ActionType.READ, false, null, false);
},
error: (err) => {
this.handleError(ActionType.READ, false, err, false);
},
});
this.getSectorUseCase
.execute(sectorId)
.pipe(first())
.subscribe({
next: (sector: Sector) => {
this.sector.set(this.sectorPresenter.toViewModel(sector));
this.handleError(ActionType.READ, false, null, false);
},
error: (err) => {
this.handleError(ActionType.READ, false, err, false);
},
});
}
private handleError(
@@ -62,5 +76,9 @@ export class SectorFacade {
) {
this.error.set({ action, hasError, message });
this.loading.set({ action, isLoading });
if (hasError) {
this.feedbackService.notify(ActionType.READ, message!, true);
}
}
}

View File

@@ -21,18 +21,19 @@ export class FeedbackService {
*/
notify(action: ActionType | null, message: string, hasError: boolean = false): void {
if (hasError) {
this.handleError();
const userFriendlyMessage = this.handleErrorMessage(message);
this.handleError(userFriendlyMessage);
} else {
this.handleSuccess(action, message);
}
}
private handleError(): void {
this.toastr.error(
`Une erreur s'est produite, veuillez réessayer ultérieurement`,
`Erreur`,
this.TOAST_CONFIG
);
private handleError(message: string): void {
this.toastr.error(`${message}`, `Erreur`, {
...this.TOAST_CONFIG,
disableTimeOut: true,
progressBar: false,
});
}
private handleSuccess(action: ActionType | null, message: string): void {
@@ -52,4 +53,30 @@ export class FeedbackService {
this.toastr.success(message, title, this.TOAST_CONFIG);
}
private handleErrorMessage(messageError: string): string {
const lowerMessage = messageError.toLowerCase();
// Mapping des erreurs PocketBase courantes vers messages utilisateurs
const errorMessages: Record<string, string> = {
'failed to authenticate': `Votre email n'est pas valide ou votre mot de passe est incorrect`,
'failed to auth': `Échec de l'authentification. Vérifiez vos identifiants.`,
'invalid email': `L'adresse email n'est pas valide.`,
'invalid password': `Le mot de passe ne respecte pas les critères de sécurité.`,
'passwords do not match': `Les mots de passe saisis ne correspondent pas.`,
'record not found': `La ressource demandée n'existe pas.`,
'validation error': `Données invalides. Vérifiez les champs saisis.`,
'network error': `Problème de connexion. Vérifiez votre réseau.`,
};
// Recherche du premier match
for (const [errorKey, userMessage] of Object.entries(errorMessages)) {
if (lowerMessage.includes(errorKey)) {
return userMessage;
}
}
// Message par défaut
return `Une erreur s'est produite, veuillez réessayer ultérieurement`;
}
}

View File

@@ -7,9 +7,12 @@ import { ActionType } from '@app/domain/action-type.util';
import { ErrorResponse } from '@app/domain/error-response.util';
import { UserViewModel } from '@app/ui/users/user.presenter.model';
import { UserPresenter } from '@app/ui/users/user.presenter';
import { first } from 'rxjs';
import { FeedbackService } from '@app/ui/shared/services/feedback.service';
@Injectable()
export class UserFacade {
private readonly feedbackService = inject(FeedbackService);
private readonly userRepository = inject(USER_REPOSITORY_TOKEN);
private readonly getUseCase = new GetUserUseCase(this.userRepository);
@@ -28,28 +31,37 @@ export class UserFacade {
loadOne(userId: string) {
this.handleError(ActionType.READ, false, null, true);
this.getUseCase.execute(userId).subscribe({
next: (user) => {
this.user.set(this.userPresenter.toViewModel(user));
this.handleError(ActionType.READ, false, null, false);
},
error: (err) => {
this.handleError(ActionType.READ, false, err, false);
},
});
this.getUseCase
.execute(userId)
.pipe(first())
.subscribe({
next: (user) => {
this.user.set(this.userPresenter.toViewModel(user));
this.handleError(ActionType.READ, false, null, false);
},
error: (err) => {
this.handleError(ActionType.READ, false, err, false);
},
});
}
update(userId: string, user: Partial<UserViewModel>) {
this.handleError(ActionType.UPDATE, false, null, true);
this.updateUseCase.execute(userId, user).subscribe({
next: (user) => {
this.user.set(this.userPresenter.toViewModel(user));
this.handleError(ActionType.UPDATE, false, null, false);
},
error: (err) => {
this.handleError(ActionType.UPDATE, false, err, false);
},
});
this.updateUseCase
.execute(userId, user)
.pipe(first())
.subscribe({
next: (user) => {
this.user.set(this.userPresenter.toViewModel(user));
this.handleError(ActionType.UPDATE, false, null, false);
const message = `Votre profile a bien été modifier !`;
this.feedbackService.notify(ActionType.UPDATE, message);
},
error: (err) => {
this.handleError(ActionType.UPDATE, false, err, false);
},
});
}
private handleError(
@@ -60,5 +72,9 @@ export class UserFacade {
) {
this.error.set({ action, hasError, message });
this.loading.set({ action, isLoading });
if (hasError) {
this.feedbackService.notify(ActionType.READ, message!, true);
}
}
}