From 31e2cbd1a6bc5fc7a1dbeaab6dbe44a198d94fd6 Mon Sep 17 00:00:00 2001 From: haoinefatma Date: Sat, 21 Feb 2026 12:39:10 +0000 Subject: [PATCH] feat: add landlord dashboard frontend --- src/app/app.routes.ts | 9 +- .../landlord-dashboard.component.html | 129 ++++++ .../landlord-dashboard.component.scss | 411 ++++++++++++++++++ .../landlord-dashboard.component.spec.ts | 23 + .../landlord-dashboard.component.ts | 61 +++ .../model/landlord-dashboard.model.ts | 27 ++ src/app/landlord/model/listing-vo.model.ts | 12 + src/app/landlord/model/picture.model.ts | 5 + .../landlord-dashboard.service.spec.ts | 16 + .../service/landlord-dashboard.service.ts | 18 + src/app/landlord/service/landlord.service.ts | 18 + src/app/layout/navbar/navbar.component.ts | 95 ++-- 12 files changed, 779 insertions(+), 45 deletions(-) create mode 100644 src/app/landlord/landlord-dashboard/landlord-dashboard.component.html create mode 100644 src/app/landlord/landlord-dashboard/landlord-dashboard.component.scss create mode 100644 src/app/landlord/landlord-dashboard/landlord-dashboard.component.spec.ts create mode 100644 src/app/landlord/landlord-dashboard/landlord-dashboard.component.ts create mode 100644 src/app/landlord/model/landlord-dashboard.model.ts create mode 100644 src/app/landlord/service/landlord-dashboard.service.spec.ts create mode 100644 src/app/landlord/service/landlord-dashboard.service.ts create mode 100644 src/app/landlord/service/landlord.service.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 15b6eba..b6a8e3d 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -8,6 +8,7 @@ import {ReservationComponent} from "./landlord/reservation/reservation.component import { EmailVerifiedComponent } from './email-verified/email-verified.component'; import { PaymentSuccessComponent } from './tenant/payment-success/payment-success.component'; import { PaymentCancelComponent } from './tenant/payment-cancel/payment-cancel.component'; +import { LandlordDashboardComponent } from './landlord/landlord-dashboard/landlord-dashboard.component'; export const routes: Routes = [ { @@ -49,5 +50,11 @@ export const routes: Routes = [ { path: 'payment-cancel', component: PaymentCancelComponent - } + }, + { + path: 'landlord/dashboard', + component: LandlordDashboardComponent, + canActivate: [authorityRouteAccess], + data: { authorities: ['ROLE_LANDLORD'] } +}, ]; \ No newline at end of file diff --git a/src/app/landlord/landlord-dashboard/landlord-dashboard.component.html b/src/app/landlord/landlord-dashboard/landlord-dashboard.component.html new file mode 100644 index 0000000..fa04a7a --- /dev/null +++ b/src/app/landlord/landlord-dashboard/landlord-dashboard.component.html @@ -0,0 +1,129 @@ +
+ + +
+

🏠 Dashboard Propriétaire

+ +
+ + +
+
+

Chargement du dashboard...

+
+ + +
+

❌ {{ error }}

+ +
+ + +
+ + +
+ + + +
+
🏠
+
+

{{ dashboard.totalProperties }}

+

Propriétés

+
+
+ +
+
📅
+
+

{{ dashboard.totalActiveBookings }}

+

Réservations Actives

+
+
+ +
+ + +
+

💳 Statut des Paiements

+
+ +
+
+
+

{{ dashboard.paymentStatus.totalPendingPayments }}

+

En attente

+ {{ dashboard.paymentStatus.pendingAmount }} MAD +
+
+ +
+
+
+

{{ dashboard.paymentStatus.totalCompletedPayments }}

+

Complétés

+ {{ dashboard.paymentStatus.completedAmount }} MAD +
+
+ +
+
+
+

{{ dashboard.paymentStatus.totalFailedPayments }}

+

Échoués

+
+
+ +
+
+ + +
+

🏡 Mes Propriétés

+ +
+

😔 Aucune propriété trouvée

+ Ajouter une propriété +
+ +
+
+ + +
+ + + +
+

{{ property.title }}

+ +
+
+ 📅 + {{ property.totalBookings }} réservations +
+
+ + {{ property.availableDays }} jours disponibles +
+
+ +
+ 🗓️ Prochain check-in : {{ property.nextCheckIn | date:'dd/MM/yyyy' }} +
+ +
+ {{ getStatusLabel(property.status) }} +
+
+ +
+
+
+ +
+
\ No newline at end of file diff --git a/src/app/landlord/landlord-dashboard/landlord-dashboard.component.scss b/src/app/landlord/landlord-dashboard/landlord-dashboard.component.scss new file mode 100644 index 0000000..9950b4f --- /dev/null +++ b/src/app/landlord/landlord-dashboard/landlord-dashboard.component.scss @@ -0,0 +1,411 @@ +.dashboard-container { + padding: 24px; + background: #f5f7fa; + min-height: 100vh; + + // Header + .dashboard-header { + display: flex; + justify-content: space-between; + align-items: center; + background: white; + padding: 20px 30px; + border-radius: 12px; + margin-bottom: 24px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + + h1 { + margin: 0; + font-size: 28px; + color: #2c3e50; + font-weight: 600; + } + + .btn-refresh { + padding: 12px 24px; + background: #ff5a5f; + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 16px; + font-weight: 500; + transition: all 0.3s; + + &:hover { + background: #e04e53; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(255, 90, 95, 0.3); + } + } + } + + // Loading + .loading { + text-align: center; + padding: 100px 20px; + + .spinner { + width: 60px; + height: 60px; + border: 5px solid #f3f3f3; + border-top: 5px solid #ff5a5f; + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto 20px; + } + + p { + font-size: 18px; + color: #666; + } + } + + @keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } + + // Error + .error { + text-align: center; + padding: 60px 20px; + background: white; + border-radius: 12px; + + p { + font-size: 18px; + color: #e74c3c; + margin-bottom: 20px; + } + + button { + padding: 12px 30px; + background: #ff5a5f; + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 16px; + transition: background 0.3s; + + &:hover { + background: #e04e53; + } + } + } + + // Global Stats + .global-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 20px; + margin-bottom: 30px; + + .stat-card { + background: white; + border-radius: 12px; + padding: 28px; + display: flex; + align-items: center; + gap: 20px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + transition: all 0.3s; + + &:hover { + transform: translateY(-5px); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12); + } + + .icon { + font-size: 48px; + } + + .info { + h2 { + margin: 0 0 6px 0; + font-size: 32px; + font-weight: bold; + color: #2c3e50; + } + + p { + margin: 0; + font-size: 15px; + color: #7f8c8d; + } + } + + &.revenue { + border-left: 5px solid #9b59b6; + } + + &.properties { + border-left: 5px solid #e74c3c; + } + + &.bookings { + border-left: 5px solid #3498db; + } + } + } + + // Section + .section { + background: white; + border-radius: 12px; + padding: 30px; + margin-bottom: 30px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + + h2 { + margin: 0 0 24px 0; + color: #2c3e50; + font-size: 22px; + font-weight: 600; + } + } + + // Payment Grid + .payment-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 20px; + + .payment-card { + border-radius: 12px; + padding: 24px; + display: flex; + align-items: center; + gap: 16px; + transition: transform 0.3s; + + &:hover { + transform: scale(1.02); + } + + .payment-icon { + font-size: 40px; + } + + .payment-info { + flex: 1; + + h3 { + margin: 0 0 6px 0; + font-size: 36px; + font-weight: bold; + color: #2c3e50; + } + + p { + margin: 0 0 8px 0; + font-size: 15px; + color: #555; + } + + .amount { + font-size: 16px; + font-weight: 600; + color: #27ae60; + } + } + + &.pending { + background: linear-gradient(135deg, #fff9e6 0%, #ffe69c 100%); + } + + &.completed { + background: linear-gradient(135deg, #eafaf1 0%, #c3e6cb 100%); + } + + &.failed { + background: linear-gradient(135deg, #fdf2f2 0%, #f5c6cb 100%); + } + } + } + + // Properties Grid + .properties-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 24px; + + .property-card { + border: 1px solid #e0e0e0; + border-radius: 12px; + overflow: hidden; + transition: all 0.3s; + background: white; + + &:hover { + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); + transform: translateY(-6px); + } + + .property-image { + position: relative; + height: 200px; + overflow: hidden; + + img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s; + } + + &:hover img { + transform: scale(1.05); + } + + .occupancy-badge { + position: absolute; + top: 15px; + right: 15px; + padding: 8px 16px; + border-radius: 20px; + font-weight: bold; + font-size: 14px; + background: white; + box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2); + + &.high { + color: #27ae60; + border: 2px solid #27ae60; + } + + &.medium { + color: #f39c12; + border: 2px solid #f39c12; + } + + &.low { + color: #e74c3c; + border: 2px solid #e74c3c; + } + } + } + + .property-details { + padding: 20px; + + h3 { + margin: 0 0 16px 0; + font-size: 19px; + color: #2c3e50; + font-weight: 600; + } + + .property-stats { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 16px; + + .stat { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + color: #666; + + .icon { + font-size: 16px; + } + } + } + + .next-checkin { + background: #e8f5e9; + padding: 12px; + border-radius: 8px; + margin-bottom: 16px; + font-size: 14px; + color: #27ae60; + font-weight: 500; + } + + .status-badge { + padding: 10px 16px; + border-radius: 20px; + font-size: 13px; + font-weight: 600; + text-align: center; + + &.status-available { + background: #d4edda; + color: #155724; + } + + &.status-occupied { + background: #f8d7da; + color: #721c24; + } + + &.status-booked-soon { + background: #fff3cd; + color: #856404; + } + } + } + } + } + + // Empty State + .empty { + text-align: center; + padding: 60px 20px; + + p { + font-size: 18px; + color: #999; + margin-bottom: 20px; + } + + a { + display: inline-block; + padding: 12px 30px; + background: #ff5a5f; + color: white; + border-radius: 8px; + text-decoration: none; + transition: background 0.3s; + + &:hover { + background: #e04e53; + } + } + } +} + +// Responsive +@media (max-width: 768px) { + .dashboard-container { + padding: 12px; + + .dashboard-header { + flex-direction: column; + gap: 16px; + padding: 16px; + + h1 { + font-size: 22px; + } + + .btn-refresh { + width: 100%; + } + } + + .global-stats, + .payment-grid, + .properties-grid { + grid-template-columns: 1fr; + } + } +} \ No newline at end of file diff --git a/src/app/landlord/landlord-dashboard/landlord-dashboard.component.spec.ts b/src/app/landlord/landlord-dashboard/landlord-dashboard.component.spec.ts new file mode 100644 index 0000000..eae8724 --- /dev/null +++ b/src/app/landlord/landlord-dashboard/landlord-dashboard.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LandlordDashboardComponent } from './landlord-dashboard.component'; + +describe('LandlordDashboardComponent', () => { + let component: LandlordDashboardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LandlordDashboardComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(LandlordDashboardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/landlord/landlord-dashboard/landlord-dashboard.component.ts b/src/app/landlord/landlord-dashboard/landlord-dashboard.component.ts new file mode 100644 index 0000000..c385a67 --- /dev/null +++ b/src/app/landlord/landlord-dashboard/landlord-dashboard.component.ts @@ -0,0 +1,61 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; +import { LandlordDashboardService } from '../service/landlord-dashboard.service'; +import { LandlordDashboardDTO } from '../model/landlord-dashboard.model'; + +@Component({ + selector: 'app-landlord-dashboard', + standalone: true, + imports: [CommonModule, RouterModule], + templateUrl: './landlord-dashboard.component.html', + styleUrls: ['./landlord-dashboard.component.scss'] +}) +export class LandlordDashboardComponent implements OnInit { + + dashboard: LandlordDashboardDTO | null = null; + loading = true; + error: string | null = null; + + constructor(private landlordDashboardService: LandlordDashboardService) {} + + ngOnInit(): void { + this.loadDashboard(); + } + + loadDashboard(): void { + this.loading = true; + this.error = null; + + this.landlordDashboardService.getDashboard().subscribe({ + next: (data) => { + this.dashboard = data; + this.loading = false; + console.log('Dashboard loaded:', data); + }, + error: (err) => { + this.error = 'Erreur lors du chargement du dashboard'; + this.loading = false; + console.error('Error:', err); + } + }); + } + + getStatusClass(status: string): string { + switch (status) { + case 'AVAILABLE': return 'status-available'; + case 'OCCUPIED': return 'status-occupied'; + case 'BOOKED_SOON': return 'status-booked-soon'; + default: return ''; + } + } + + getStatusLabel(status: string): string { + switch (status) { + case 'AVAILABLE': return '✅ Disponible'; + case 'OCCUPIED': return '🔴 Occupé'; + case 'BOOKED_SOON': return '🟡 Réservé bientôt'; + default: return status; + } + } +} \ No newline at end of file diff --git a/src/app/landlord/model/landlord-dashboard.model.ts b/src/app/landlord/model/landlord-dashboard.model.ts new file mode 100644 index 0000000..5af8626 --- /dev/null +++ b/src/app/landlord/model/landlord-dashboard.model.ts @@ -0,0 +1,27 @@ +export interface PaymentStatusDTO { + totalPendingPayments: number; + totalCompletedPayments: number; + totalFailedPayments: number; + pendingAmount: number; + completedAmount: number; + totalRevenue: number; +} + +export interface PropertyAvailabilityDTO { + propertyPublicId: string; + title: string; + coverUrl: string; + totalBookings: number; + occupancyRate: number; + availableDays: number; + nextCheckIn: string | null; + status: 'AVAILABLE' | 'OCCUPIED' | 'BOOKED_SOON'; +} + +export interface LandlordDashboardDTO { + paymentStatus: PaymentStatusDTO; + properties: PropertyAvailabilityDTO[]; + totalRevenue: number; + totalProperties: number; + totalActiveBookings: number; +} \ No newline at end of file diff --git a/src/app/landlord/model/listing-vo.model.ts b/src/app/landlord/model/listing-vo.model.ts index c945c56..def8f17 100644 --- a/src/app/landlord/model/listing-vo.model.ts +++ b/src/app/landlord/model/listing-vo.model.ts @@ -25,3 +25,15 @@ export interface DescriptionVO { export interface PriceVO { value: number } +export interface ListingVO { + publicId: string; + title: string; + description: string; + location: string; + price: number; + category: string; + bedrooms: number; + guests: number; + beds: number; + bathrooms: number; +} diff --git a/src/app/landlord/model/picture.model.ts b/src/app/landlord/model/picture.model.ts index 29e3718..6edbeea 100644 --- a/src/app/landlord/model/picture.model.ts +++ b/src/app/landlord/model/picture.model.ts @@ -2,3 +2,8 @@ export interface NewListingPicture { file: File, urlDisplay: string } +export interface Picture { + file: string; // base64 encoded or URL + fileContentType: string; + isCover: boolean; +} \ No newline at end of file diff --git a/src/app/landlord/service/landlord-dashboard.service.spec.ts b/src/app/landlord/service/landlord-dashboard.service.spec.ts new file mode 100644 index 0000000..0ec6408 --- /dev/null +++ b/src/app/landlord/service/landlord-dashboard.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { LandlordDashboardService } from './landlord-dashboard.service'; + +describe('LandlordDashboardService', () => { + let service: LandlordDashboardService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(LandlordDashboardService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/landlord/service/landlord-dashboard.service.ts b/src/app/landlord/service/landlord-dashboard.service.ts new file mode 100644 index 0000000..b31fa2c --- /dev/null +++ b/src/app/landlord/service/landlord-dashboard.service.ts @@ -0,0 +1,18 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { LandlordDashboardDTO } from '../model/landlord-dashboard.model'; + +@Injectable({ + providedIn: 'root' +}) +export class LandlordDashboardService { + + private apiUrl = 'http://localhost:8080/api/landlord'; + + constructor(private http: HttpClient) {} + + getDashboard(): Observable { + return this.http.get(`${this.apiUrl}/dashboard`); + } +} \ No newline at end of file diff --git a/src/app/landlord/service/landlord.service.ts b/src/app/landlord/service/landlord.service.ts new file mode 100644 index 0000000..b31fa2c --- /dev/null +++ b/src/app/landlord/service/landlord.service.ts @@ -0,0 +1,18 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { LandlordDashboardDTO } from '../model/landlord-dashboard.model'; + +@Injectable({ + providedIn: 'root' +}) +export class LandlordDashboardService { + + private apiUrl = 'http://localhost:8080/api/landlord'; + + constructor(private http: HttpClient) {} + + getDashboard(): Observable { + return this.http.get(`${this.apiUrl}/dashboard`); + } +} \ No newline at end of file diff --git a/src/app/layout/navbar/navbar.component.ts b/src/app/layout/navbar/navbar.component.ts index f9c7253..12c3619 100644 --- a/src/app/layout/navbar/navbar.component.ts +++ b/src/app/layout/navbar/navbar.component.ts @@ -89,51 +89,58 @@ export class NavbarComponent implements OnInit { } private fetchMenu(): MenuItem[] { - if (this.authService.isAuthenticated()) { - return [ - { - label: 'Create', - command: () => this.openNewListing(), - visible: this.hasToBeLandlord(), - }, - { - label: 'My properties', - routerLink: 'landlord/properties', - visible: this.hasToBeLandlord(), - }, - { - label: 'My booking', - routerLink: 'booking', - }, - { - label: 'My reservation', - routerLink: 'landlord/reservation', - visible: this.hasToBeLandlord(), - }, - { - label: 'Become a LandLord', - command: () => this.becomeLandlord(), - visible: !this.hasToBeLandlord(), - }, - { - label: 'Log out', - command: this.logout, - }, - ]; - } else { - return [ - { - label: 'Sign up', - styleClass: 'font-bold', - command: () => this.signUp(), - }, - { - label: 'Log in', - command: this.login, - }, - ]; - } + if (this.authService.isAuthenticated()) { + return [ + { + label: 'Create', + command: () => this.openNewListing(), + visible: this.hasToBeLandlord(), + }, + { + label: 'My properties', + routerLink: ['/landlord/properties'], + visible: this.hasToBeLandlord(), + }, + { + label: 'My Dashboard', + routerLink: ['/landlord/dashboard'], + visible: this.hasToBeLandlord(), + }, + { + label: 'My booking', + routerLink: ['/booking'], + }, + { + label: 'My reservation', + routerLink: ['/landlord/reservation'], + visible: this.hasToBeLandlord(), + }, + + { + label: 'Become a LandLord', + command: () => this.becomeLandlord(), + visible: !this.hasToBeLandlord(), + }, + { + label: 'Log out', + command: this.logout, + }, + ]; + } else { + return [ + { + label: 'Sign up', + styleClass: 'font-bold', + command: () => this.signUp(), + }, + { + label: 'Log in', + command: this.login, + }, + ]; } +} + hasToBeLandlord(): boolean { return this.authService.hasAnyAuthority('ROLE_LANDLORD');