refacto : sauvegarde des paramettres de profil en base
This commit is contained in:
@@ -3,4 +3,5 @@ import { ActionType } from '@app/domain/action-type.util';
|
||||
export interface LoaderAction {
|
||||
action: ActionType;
|
||||
isLoading: boolean;
|
||||
isDone?: boolean;
|
||||
}
|
||||
|
||||
6
src/app/domain/profiles/dto/settings-profile.dto.ts
Normal file
6
src/app/domain/profiles/dto/settings-profile.dto.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface SettingsProfileDto {
|
||||
isProfilePublic: boolean;
|
||||
showEmail: boolean;
|
||||
showPhone: boolean;
|
||||
allowGeolocation: boolean;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -503,7 +503,7 @@
|
||||
<app-my-profile-map [profile]="profile()" />
|
||||
}
|
||||
@case ('settings') {
|
||||
<app-settings />
|
||||
<app-settings [profile]="profile()" />
|
||||
}
|
||||
@case ('home') {
|
||||
@if (!userLoading().isLoading) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 }
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
|
||||
@@ -26,6 +26,12 @@ describe('ProfilePresenter', () => {
|
||||
isProfileVisible: true,
|
||||
missingFields: [],
|
||||
projets: ['p1', 'p2'],
|
||||
settings: {
|
||||
showEmail: false,
|
||||
showPhone: false,
|
||||
allowGeolocation: false,
|
||||
isProfilePublic: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
12
src/app/usecase/profiles/update-settings-profile.usecase.ts
Normal file
12
src/app/usecase/profiles/update-settings-profile.usecase.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user