Refactor application structure to use WorkspaceService for state management

- Removed direct state management from components and centralized it in WorkspaceService.
- Updated ApartmentsPage, TenantsPage, LeasesPage, and DashboardPage to utilize WorkspaceService for data handling.
- Introduced SessionService to manage user session state and authentication.
- Simplified component templates to reflect new service-based architecture.
- Removed unused ConfigService and its tests.
- Added lease.html template for lease document generation.
- Created app.routes.spec.ts for route mapping tests.
This commit is contained in:
nessar authored and Nessar committed 2026-08-17 14:18:15 +02:00
1 parent be1f680a62
commit 82f339e209
33 files changed
+292 -929

No files matched your search

+8 -226
View File
@@ -1,21 +1,9 @@
import { Component, computed, signal, inject, OnInit, effect } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ApartmentService, Apartment } from './services/apartment.service';
import { ThemeService } from './services/theme.service';
import { Component, computed, inject, OnInit } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { BrandMark } from './components/brand-mark';
import { SignInPage } from './components/sign-in-page/sign-in-page';
import {
WorkspaceNavigation,
WorkspaceTab,
} from './components/workspace-navigation/workspace-navigation';
import { DashboardPage } from './components/dashboard-page/dashboard-page';
import { ApartmentsPage } from './components/apartments-page/apartments-page';
import { ApartmentSaveRequest } from './components/apartments-page/apartments-page';
import { SettingsPage } from './components/settings-page/settings-page';
import { Tenant, TenantService } from './services/tenant.service';
import { Lease, LeaseService } from './services/lease.service';
import { TenantsPage, TenantSaveRequest } from './components/tenants-page/tenants-page';
import { LeasesPage } from './components/leases-page/leases-page';
import { WorkspaceNavigation } from './components/workspace-navigation/workspace-navigation';
import { SessionService } from './services/session.service';
@Component({
selector: 'app-root',
@@ -23,81 +11,17 @@ import { LeasesPage } from './components/leases-page/leases-page';
BrandMark,
SignInPage,
WorkspaceNavigation,
DashboardPage,
ApartmentsPage,
TenantsPage,
LeasesPage,
SettingsPage,
RouterOutlet,
],
templateUrl: './app.html',
styleUrl: './app.css',
})
export class App implements OnInit {
protected readonly title = signal('LMNP Organizer');
readonly helloMessage = signal<string>('');
// Tab & CRUD States
readonly activeTab = signal<WorkspaceTab>('dashboard');
readonly apartments = signal<Apartment[]>([]);
readonly tenants = signal<Tenant[]>([]);
readonly leases = signal<Lease[]>([]);
readonly tenantAttachmentError = signal('');
private readonly _isLoggedIn = signal<boolean>(false);
private readonly _username = signal<string>('');
private readonly _email = signal<string>('');
readonly userInitial = computed(() => this._username().charAt(0).toUpperCase());
private readonly http = inject(HttpClient);
private readonly apartmentService = inject(ApartmentService);
private readonly tenantService = inject(TenantService);
private readonly leaseService = inject(LeaseService);
protected readonly themeService = inject(ThemeService);
readonly themePreference = this.themeService.preference;
constructor() {
effect(() => {
if (this.isLoggedIn) {
this.loadApartments();
this.loadTenants();
this.loadLeases();
}
});
}
readonly session = inject(SessionService);
readonly userInitial = computed(() => this.session.username().charAt(0).toUpperCase());
ngOnInit() {
this.http
.get<{ authenticated: boolean; username?: string; email?: string }>('/api/user')
.subscribe({
next: (user) => {
if (user.authenticated) {
this._isLoggedIn.set(true);
this._username.set(user.username || 'User');
this._email.set(user.email || '');
} else {
this._isLoggedIn.set(false);
this._username.set('');
this._email.set('');
}
},
error: (err) => {
console.error('Error fetching user authentication status', err);
this._isLoggedIn.set(false);
this._username.set('');
this._email.set('');
},
});
}
get isLoggedIn(): boolean {
return this._isLoggedIn();
}
get username(): string {
return this._username();
}
get email(): string {
return this._email();
this.session.refresh();
}
login() {
@@ -108,146 +32,4 @@ export class App implements OnInit {
window.location.href = '/logout';
}
callHelloApi() {
this.http.get('/api/hello', { responseType: 'text' }).subscribe({
next: (msg) => this.helloMessage.set(msg),
error: (err) => {
console.error(err);
this.helloMessage.set('Failed to connect to backend: ' + err.statusText);
},
});
}
// Apartment CRUD operations
loadApartments() {
this.apartmentService.getApartments().subscribe({
next: (data) => this.apartments.set(data),
error: (err) => console.error('Error loading apartments', err),
});
}
saveApartment({ apartment, apartmentId }: ApartmentSaveRequest): void {
if (apartmentId) {
this.apartmentService.updateApartment(apartmentId, apartment).subscribe({
next: () => {
this.loadApartments();
},
error: (err) => console.error('Error updating apartment', err),
});
} else {
this.apartmentService.createApartment(apartment).subscribe({
next: () => {
this.loadApartments();
},
error: (err) => console.error('Error creating apartment', err),
});
}
}
deleteApartment(id: string) {
if (confirm('Êtes-vous sûr de vouloir supprimer cet appartement ?')) {
this.apartmentService.deleteApartment(id).subscribe({
next: () => this.loadApartments(),
error: (err) => console.error('Error deleting apartment', err),
});
}
}
loadTenants(): void {
this.tenantService.getTenants().subscribe({
next: (data) => this.tenants.set(data),
error: (err) => console.error('Error loading tenants', err),
});
}
saveTenant({ tenant, tenantId }: TenantSaveRequest): void {
const request = tenantId
? this.tenantService.updateTenant(tenantId, tenant)
: this.tenantService.createTenant(tenant);
request.subscribe({
next: () => this.loadTenants(),
error: (err) => console.error('Error saving tenant', err),
});
}
deleteTenant(id: string): void {
if (confirm('Êtes-vous sûr de vouloir supprimer ce locataire ?')) {
this.tenantService.deleteTenant(id).subscribe({
next: () => this.loadTenants(),
error: (err) => console.error('Error deleting tenant', err),
});
}
}
uploadTenantAttachment(request: { tenantId: string; file: File }): void {
this.tenantAttachmentError.set('');
this.tenantService.uploadAttachment(request.tenantId, request.file).subscribe({
next: () => this.loadTenants(),
error: (error: unknown) => {
console.error('Error uploading tenant attachment', error);
this.tenantAttachmentError.set(this.errorMessage(error, 'Impossible d’envoyer cette pièce jointe.'));
},
});
}
downloadTenantAttachment(request: {
tenantId: string;
attachment: { id: string; fileName: string };
}): void {
this.tenantService.downloadAttachment(request.tenantId, request.attachment.id).subscribe({
next: (blob) => this.saveBlob(blob, request.attachment.fileName),
error: (err) => console.error('Error downloading tenant attachment', err),
});
}
loadLeases(): void {
this.leaseService.getLeases().subscribe({
next: (data) => this.leases.set(data),
error: (err) => console.error('Error loading leases', err),
});
}
saveLease(lease: Lease): void {
this.leaseService.createLease(lease).subscribe({
next: () => this.loadLeases(),
error: (err) => console.error('Error saving lease', err),
});
}
deleteLease(id: string): void {
if (confirm('Êtes-vous sûr de vouloir supprimer ce bail ?')) {
this.leaseService.deleteLease(id).subscribe({
next: () => this.loadLeases(),
error: (err) => console.error('Error deleting lease', err),
});
}
}
downloadLeasePdf(lease: Lease): void {
if (!lease.id) return;
this.leaseService.downloadPdf(lease.id).subscribe({
next: (blob) => this.saveBlob(blob, `bail-${lease.id}.pdf`),
error: (err) => console.error('Error downloading lease PDF', err),
});
}
private saveBlob(blob: Blob, fileName: string): void {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
link.click();
URL.revokeObjectURL(url);
}
private errorMessage(error: unknown, fallback: string): string {
if (typeof error !== 'object' || error === null || !('error' in error)) {
return fallback;
}
const response = error.error;
if (typeof response !== 'object' || response === null || !('message' in response)) {
return fallback;
}
return typeof response.message === 'string' ? response.message : fallback;
}
}