refacto : sauvegarde des paramettres de profil en base

This commit is contained in:
styve Lioumba
2025-12-14 00:44:51 +01:00
parent e4f5cb8938
commit de34c91f09
18 changed files with 181 additions and 33 deletions

View File

@@ -3,4 +3,5 @@ import { ActionType } from '@app/domain/action-type.util';
export interface LoaderAction {
action: ActionType;
isLoading: boolean;
isDone?: boolean;
}

View File

@@ -0,0 +1,6 @@
export interface SettingsProfileDto {
isProfilePublic: boolean;
showEmail: boolean;
showPhone: boolean;
allowGeolocation: boolean;
}

View File

@@ -12,6 +12,10 @@ export interface Profile {
projets: string[];
apropos: string;
coordonnees?: { lat: number; lon: number };
estVisible?: boolean;
estGeolocaliser?: boolean;
partageMail?: boolean;
partagePhone?: boolean;
}
export interface ProfilePaginated {

View File

@@ -1,6 +1,7 @@
import { Profile, ProfilePaginated } from '@app/domain/profiles/profile.model';
import { Observable } from 'rxjs';
import { SearchFilters } from '@app/domain/search/search-filters';
import { SettingsProfileDto } from '@app/domain/profiles/dto/settings-profile.dto';
export interface ProfileRepository {
list(params?: SearchFilters): Observable<ProfilePaginated>;
@@ -9,4 +10,5 @@ export interface ProfileRepository {
create(profile: Profile): Observable<Profile>;
update(profileId: string, profile: Partial<Profile>): Observable<Profile>;
updateCoordinates(profileId: string, latitude: number, longitude: number): Observable<Profile>;
updateSettings(profileId: string, settings: SettingsProfileDto): Observable<Profile>;
}

View File

@@ -1,3 +1,5 @@
import { SettingsProfileDto } from '@app/domain/profiles/dto/settings-profile.dto';
export enum ThemeType {
LIGHT = 'light',
DARK = 'dark',
@@ -6,12 +8,7 @@ export enum ThemeType {
export interface UserSettings {
theme: ThemeType;
privacy: {
isProfilePublic: boolean;
showEmail: boolean;
showPhone: boolean;
allowGeolocation: boolean;
};
privacy: SettingsProfileDto;
}
// Valeurs par défaut pour éviter les nulls

View File

@@ -6,6 +6,7 @@ import PocketBase from 'pocketbase';
import { environment } from '@env/environment';
import { ProfileDTO } from '@app/domain/profiles/dto/create-profile.dto';
import { SearchFilters } from '@app/domain/search/search-filters';
import { SettingsProfileDto } from '@app/domain/profiles/dto/settings-profile.dto';
@Injectable({ providedIn: 'root' })
export class PbProfileRepository implements ProfileRepository {
@@ -58,6 +59,16 @@ export class PbProfileRepository implements ProfileRepository {
);
}
updateSettings(profileId: string, settings: SettingsProfileDto): Observable<Profile> {
const settingsData = {
estVisible: settings.isProfilePublic,
estGeolocaliser: settings.allowGeolocation,
partageMail: settings.showEmail,
partagePhone: settings.showPhone,
};
return from(this.pb.collection('profiles').update<Profile>(profileId, settingsData));
}
private onFilterSetting(params?: SearchFilters): string[] {
const filters: string[] = [
'utilisateur.verified = true',

View File

@@ -503,7 +503,7 @@
<app-my-profile-map [profile]="profile()" />
}
@case ('settings') {
<app-settings />
<app-settings [profile]="profile()" />
}
@case ('home') {
@if (!userLoading().isLoading) {

View File

@@ -4,17 +4,24 @@ import { MyProfileMapComponent } from './my-profile-map.component';
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 { ToastrService } from 'ngx-toastr';
import { mockToastR } from '@app/testing/toastr.mock';
describe('MyProfileMapComponent', () => {
let component: MyProfileMapComponent;
let fixture: ComponentFixture<MyProfileMapComponent>;
let mockProfileRepository: jest.Mocked<Partial<ProfileRepository>> = mockProfileRepo;
let mockToastrService: jest.Mocked<Partial<ToastrService>> = mockToastR;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MyProfileMapComponent],
providers: [{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepository }],
providers: [
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepository },
{ provide: ToastrService, useValue: mockToastrService },
],
}).compileComponents();
fixture = TestBed.createComponent(MyProfileMapComponent);

View File

@@ -4,6 +4,8 @@ import { Coordinates, CoordinatesValidator } from '@app/domain/localisation/coor
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',
@@ -17,6 +19,10 @@ 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;
protected readonly locationState = this.locationFacade.locationState;
protected readonly markers = signal<MapMarker[]>([]);
@@ -42,6 +48,20 @@ export class MyProfileMapComponent implements OnInit {
if (state.coordinates) {
this.updateMarkers(state.coordinates);
}
if (!this.loading().isLoading) {
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;
}
}
},
{ allowSignalWrites: true }
);

View File

@@ -7,6 +7,9 @@ import { SETTING_REPOSITORY_TOKEN } from '@app/infrastructure/settings/setting-r
import { mockToastR } from '@app/testing/toastr.mock';
import { SettingRepository } from '@app/domain/settings/setting.repository';
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';
describe('SettingsComponent', () => {
let component: SettingsComponent;
@@ -14,6 +17,7 @@ describe('SettingsComponent', () => {
let mockToastrService: jest.Mocked<Partial<ToastrService>> = mockToastR;
let mockSettingRepository: jest.Mocked<Partial<SettingRepository>> = mockSettingRepo;
let mockProfileRepository: jest.Mocked<Partial<ProfileRepository>> = mockProfileRepo;
beforeEach(async () => {
await TestBed.configureTestingModule({
@@ -21,6 +25,7 @@ describe('SettingsComponent', () => {
providers: [
provideRouter([]),
{ provide: SETTING_REPOSITORY_TOKEN, useValue: mockSettingRepository },
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepository },
{ provide: ToastrService, useValue: mockToastrService },
],
}).compileComponents();

View File

@@ -1,8 +1,12 @@
import { Component, effect, inject, OnInit } from '@angular/core';
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';
import { SettingsProfileDto } from '@app/domain/profiles/dto/settings-profile.dto';
@Component({
selector: 'app-settings',
@@ -16,6 +20,10 @@ export class SettingsComponent implements OnInit {
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>();
settings = this.settingsFacade.settings;
@@ -33,20 +41,26 @@ export class SettingsComponent implements OnInit {
constructor() {
effect(() => {
const userSettings = this.settings();
const userSettings = this.profile()!.settings
? this.profile()!.settings!
: this.settings().privacy;
if (userSettings) {
this.settingsForm.patchValue(
{
theme: userSettings.theme,
privacy: {
isProfilePublic: userSettings.privacy.isProfilePublic,
showEmail: userSettings.privacy.showEmail,
showPhone: userSettings.privacy.showPhone,
allowGeolocation: userSettings.privacy.allowGeolocation,
},
},
{ emitEvent: false }
);
this.updateForm(userSettings);
}
if (!this.loading().isLoading) {
switch (this.loading().action) {
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;
}
}
});
}
@@ -59,18 +73,20 @@ export class SettingsComponent implements OnInit {
if (this.settingsForm.invalid) return;
const settingsFormValue = this.settingsForm.getRawValue();
const privacyFormValue = {
isProfilePublic: !!settingsFormValue.privacy.isProfilePublic,
showEmail: !!settingsFormValue.privacy.showEmail,
showPhone: !!settingsFormValue.privacy.showPhone,
allowGeolocation: !!settingsFormValue.privacy.allowGeolocation,
};
this.profileFacade.updateSettings(this.profile()!.id, privacyFormValue);
this.settingsFacade.updateSettings({
...this.settings()!,
theme: settingsFormValue.theme!,
privacy: {
isProfilePublic: !!settingsFormValue.privacy.isProfilePublic,
showEmail: !!settingsFormValue.privacy.showEmail,
showPhone: !!settingsFormValue.privacy.showPhone,
allowGeolocation: !!settingsFormValue.privacy.allowGeolocation,
},
privacy: privacyFormValue,
});
this.feedbackService.notify(null, 'Vos paramètres ont été enregistrés.', false);
}
onCancel() {
@@ -87,4 +103,19 @@ export class SettingsComponent implements OnInit {
this.settingsForm.patchValue({ theme: themeMode });
this.settingsFacade.applyThemeSettings({ ...this.settings(), theme: themeMode });
}
private updateForm(userSettings: SettingsProfileDto) {
this.settingsForm.patchValue(
{
theme: this.settings().theme,
privacy: {
isProfilePublic: userSettings.isProfilePublic ?? false,
showEmail: userSettings.showEmail,
showPhone: userSettings.showPhone,
allowGeolocation: userSettings.allowGeolocation,
},
},
{ emitEvent: false }
);
}
}

View File

@@ -2,6 +2,7 @@ import { Profile, ProfilePaginated } from '@app/domain/profiles/profile.model';
import { mockProfilePaginated, mockProfiles } from '@app/testing/profile.mock';
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
import { Observable, of } from 'rxjs';
import { SettingsProfileDto } from '@app/domain/profiles/dto/settings-profile.dto';
export class FakeProfileRepository implements ProfileRepository {
list(): Observable<ProfilePaginated> {
@@ -32,4 +33,15 @@ export class FakeProfileRepository implements ProfileRepository {
const coordinate = { latitude, longitude };
return of({ ...existing, coordinate });
}
updateSettings(profileId: string, settings: SettingsProfileDto): Observable<Profile> {
const existing = mockProfiles.find((p) => p.id === profileId) ?? mockProfiles[0];
const settingsData = {
estVisible: settings.isProfilePublic,
estGeolocaliser: settings.allowGeolocation,
partageMail: settings.showEmail,
partagePhone: settings.showPhone,
};
return of({ ...existing, settingsData });
}
}

View File

@@ -45,4 +45,6 @@ export const mockProfileRepo = {
list: jest.fn().mockReturnValue(of([])),
getById: jest.fn().mockReturnValue(of({} as Profile)),
update: jest.fn().mockReturnValue(of({} as Profile)),
updateCoordinates: jest.fn(),
updateSettings: jest.fn(),
};

View File

@@ -26,6 +26,12 @@ describe('ProfilePresenter', () => {
isProfileVisible: true,
missingFields: [],
projets: ['p1', 'p2'],
settings: {
showEmail: false,
showPhone: false,
allowGeolocation: false,
isProfilePublic: false,
},
});
});

View File

@@ -14,6 +14,8 @@ import { ProfileDTO } from '@app/domain/profiles/dto/create-profile.dto';
import { SearchFilters } from '@app/domain/search/search-filters';
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';
@Injectable({
providedIn: 'root',
@@ -27,13 +29,18 @@ export class ProfileFacade {
private createUseCase = new CreateProfileUseCase(this.profileRepository);
private updateUseCase = new UpdateProfileUseCase(this.profileRepository);
private updateCoordinateUseCase = new UpdateCoordinateProfileUseCase(this.profileRepository);
private updateSettingsUseCase = new UpdateSettingsProfileUseCase(this.profileRepository);
private getUseCase = new GetProfileUseCase(this.profileRepository);
readonly searchFilters = this.searchService.getFilters();
readonly profiles = signal<ProfileViewModel[]>([]);
readonly profilePaginated = signal<ProfilePaginated>({} as ProfilePaginated);
readonly profile = signal<ProfileViewModel>({} as ProfileViewModel);
readonly loading = signal<LoaderAction>({ isLoading: false, action: ActionType.NONE });
readonly loading = signal<LoaderAction>({
isLoading: false,
action: ActionType.NONE,
isDone: false,
});
readonly error = signal<ErrorResponse>({
action: ActionType.NONE,
hasError: false,
@@ -138,13 +145,28 @@ export class ProfileFacade {
});
}
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);
},
});
}
private handleError(
action: ActionType = ActionType.NONE,
hasError: boolean,
message: string | null = null,
isLoading = false
isLoading = false,
isDone = false
) {
this.error.set({ action, hasError, message });
this.loading.set({ action, isLoading });
this.loading.set({ action, isLoading, isDone });
}
}

View File

@@ -1,4 +1,5 @@
import { Coordinates } from '@app/domain/localisation/coordinates.model';
import { SettingsProfileDto } from '@app/domain/profiles/dto/settings-profile.dto';
export interface ProfileViewModel {
id: string;
@@ -18,4 +19,5 @@ export interface ProfileViewModel {
isProfileVisible?: boolean;
missingFields?: string[];
coordonnees?: Coordinates;
settings?: SettingsProfileDto;
}

View File

@@ -1,10 +1,17 @@
import { ProfileViewModel } from '@app/ui/profiles/profile.presenter.model';
import { Profile } from '@app/domain/profiles/profile.model';
import { SettingsProfileDto } from '@app/domain/profiles/dto/settings-profile.dto';
export class ProfilePresenter {
toViewModel(profile: Profile): ProfileViewModel {
const isProfileVisible = this.isProfileVisible(profile);
const missingFields = this.missingFields(profile);
const settings: SettingsProfileDto = {
showEmail: profile.partageMail ?? false,
showPhone: profile.partagePhone ?? false,
allowGeolocation: profile.estGeolocaliser ?? false,
isProfilePublic: profile.estVisible ?? false,
};
return {
id: profile.id,
@@ -24,6 +31,7 @@ export class ProfilePresenter {
coordonnees: profile.coordonnees
? { latitude: profile!.coordonnees!.lat!, longitude: profile!.coordonnees!.lon! }
: undefined,
settings,
isProfileVisible,
missingFields,
};

View File

@@ -0,0 +1,12 @@
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
import { Profile } from '@app/domain/profiles/profile.model';
import { Observable } from 'rxjs';
import { SettingsProfileDto } from '@app/domain/profiles/dto/settings-profile.dto';
export class UpdateSettingsProfileUseCase {
constructor(private readonly repo: ProfileRepository) {}
execute(profileId: string, settingsDto: SettingsProfileDto): Observable<Profile> {
return this.repo.updateSettings(profileId, settingsDto);
}
}