From 47cb4bcdaf7ac065a3369b7956a71547a46ec3ba Mon Sep 17 00:00:00 2001 From: TO Date: Fri, 17 Jul 2026 00:34:59 +0200 Subject: [PATCH] feat: add rent payment schedules and receipts --- CONTEXT.md | 5 +- .../lmnporganizer/backend/SecurityConfig.java | 1 + .../controller/RentPaymentController.java | 37 ++++ .../backend/dto/RentPaymentResponse.java | 19 ++ .../backend/model/RentPayment.java | 65 +++++++ .../backend/model/RentPaymentStatus.java | 7 + .../backend/repository/LeaseRepository.java | 4 + .../repository/RentPaymentRepository.java | 14 ++ .../backend/service/RentPaymentService.java | 125 +++++++++++++ .../service/RentReceiptPdfService.java | 55 ++++++ .../resources/templates/rent-receipt.html | 31 ++++ .../controller/RentPaymentControllerTest.java | 138 +++++++++++++++ .../backend/model/ModelCoverageTest.java | 30 ++++ .../service/RentPaymentServiceTest.java | 164 ++++++++++++++++++ frontend/src/app/app.routes.spec.ts | 1 + frontend/src/app/app.routes.ts | 2 + .../rent-payments-page/rent-payments-page.css | 63 +++++++ .../rent-payments-page.html | 100 +++++++++++ .../rent-payments-page.spec.ts | 49 ++++++ .../rent-payments-page/rent-payments-page.ts | 59 +++++++ .../workspace-navigation.html | 10 ++ .../workspace-navigation.ts | 2 + .../app/services/rent-payment.service.spec.ts | 27 +++ .../src/app/services/rent-payment.service.ts | 34 ++++ .../app/services/workspace.service.spec.ts | 30 +++- .../src/app/services/workspace.service.ts | 49 ++++++ 26 files changed, 1118 insertions(+), 3 deletions(-) create mode 100644 backend/src/main/java/fr/nessar/lmnporganizer/backend/controller/RentPaymentController.java create mode 100644 backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/RentPaymentResponse.java create mode 100644 backend/src/main/java/fr/nessar/lmnporganizer/backend/model/RentPayment.java create mode 100644 backend/src/main/java/fr/nessar/lmnporganizer/backend/model/RentPaymentStatus.java create mode 100644 backend/src/main/java/fr/nessar/lmnporganizer/backend/repository/RentPaymentRepository.java create mode 100644 backend/src/main/java/fr/nessar/lmnporganizer/backend/service/RentPaymentService.java create mode 100644 backend/src/main/java/fr/nessar/lmnporganizer/backend/service/RentReceiptPdfService.java create mode 100644 backend/src/main/resources/templates/rent-receipt.html create mode 100644 backend/src/test/java/fr/nessar/lmnporganizer/backend/controller/RentPaymentControllerTest.java create mode 100644 backend/src/test/java/fr/nessar/lmnporganizer/backend/service/RentPaymentServiceTest.java create mode 100644 frontend/src/app/components/rent-payments-page/rent-payments-page.css create mode 100644 frontend/src/app/components/rent-payments-page/rent-payments-page.html create mode 100644 frontend/src/app/components/rent-payments-page/rent-payments-page.spec.ts create mode 100644 frontend/src/app/components/rent-payments-page/rent-payments-page.ts create mode 100644 frontend/src/app/services/rent-payment.service.spec.ts create mode 100644 frontend/src/app/services/rent-payment.service.ts diff --git a/CONTEXT.md b/CONTEXT.md index 0d70952..68e061c 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -16,6 +16,10 @@ _Avoid_: Caution, caution solidaire Contrat de location écrit liant le propriétaire à un ou plusieurs locataires (co-signataires) pour un appartement donné, définissant les conditions de la location (loyer, charges, durée). _Avoid_: Contrat, convention de location +**Échéance de loyer** (Code: `RentPayment`): +Loyer mensuel d'un bail actif, généré à partir de ses conditions et suivi comme à venir, en retard ou payé. +_Avoid_: Paiement, mensualité + **Amortissement** (Code: `Amortization`): Perte de valeur comptable d'un actif (bâtiment décomposé en composants, mobilier) étalée sur sa durée d'utilisation théorique, déductible fiscalement sous le régime réel. _Avoid_: Dépréciation, usure @@ -100,4 +104,3 @@ _Avoid_: Mise de côté, suppression logique **Purge** (Code: `DataPurging`): Action de suppression définitive et irréversible de fichiers ou données personnelles sensibles (justificatifs de loyers, photos d'états des lieux, dossiers de candidats) à l'expiration de leur durée de conservation légale RGPD. _Avoid_: Nettoyage, effacement temporaire - diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/SecurityConfig.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/SecurityConfig.java index bb051de..91474c5 100644 --- a/backend/src/main/java/fr/nessar/lmnporganizer/backend/SecurityConfig.java +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/SecurityConfig.java @@ -22,6 +22,7 @@ public class SecurityConfig { .requestMatchers("/api/apartments", "/api/apartments/**").authenticated() .requestMatchers("/api/tenants", "/api/tenants/**").authenticated() .requestMatchers("/api/leases", "/api/leases/**").authenticated() + .requestMatchers("/api/rent-payments", "/api/rent-payments/**").authenticated() .requestMatchers("/api/user").permitAll() .anyRequest().permitAll() ) diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/controller/RentPaymentController.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/controller/RentPaymentController.java new file mode 100644 index 0000000..14eb4bd --- /dev/null +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/controller/RentPaymentController.java @@ -0,0 +1,37 @@ +package fr.nessar.lmnporganizer.backend.controller; + +import fr.nessar.lmnporganizer.backend.dto.RentPaymentResponse; +import fr.nessar.lmnporganizer.backend.service.RentPaymentService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/rent-payments") +public class RentPaymentController { + private final RentPaymentService rentPaymentService; + + public RentPaymentController(RentPaymentService rentPaymentService) { + this.rentPaymentService = rentPaymentService; + } + + @GetMapping + public List getRentPayments() { + return rentPaymentService.getAll(); + } + + @PutMapping("/{id}/paid") + public ResponseEntity markPaid(@PathVariable String id) { + return rentPaymentService.markPaid(id); + } + + @GetMapping("/{id}/receipt") + public ResponseEntity downloadReceipt(@PathVariable String id) { + return rentPaymentService.downloadReceipt(id); + } +} diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/RentPaymentResponse.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/RentPaymentResponse.java new file mode 100644 index 0000000..c2ac656 --- /dev/null +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/RentPaymentResponse.java @@ -0,0 +1,19 @@ +package fr.nessar.lmnporganizer.backend.dto; + +import fr.nessar.lmnporganizer.backend.model.RentPaymentStatus; + +import java.math.BigDecimal; +import java.time.LocalDate; + +public record RentPaymentResponse( + String id, + String leaseId, + String month, + LocalDate dueDate, + BigDecimal rent, + BigDecimal charges, + BigDecimal total, + RentPaymentStatus status, + LocalDate paidAt +) { +} diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/model/RentPayment.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/model/RentPayment.java new file mode 100644 index 0000000..9eef33d --- /dev/null +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/model/RentPayment.java @@ -0,0 +1,65 @@ +package fr.nessar.lmnporganizer.backend.model; + +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.CompoundIndex; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@Document(collection = "rentPayments") +@CompoundIndex(name = "lease_month", def = "{'leaseId': 1, 'month': 1}", unique = true) +public class RentPayment { + @Id + private String id; + + @Indexed + private String landlordId; + + @Indexed + private String leaseId; + + private String month; + private LocalDate dueDate; + private BigDecimal rent; + private BigDecimal charges; + private LocalDate paidAt; + + public RentPayment() { + } + + public RentPayment(String id, String landlordId, String leaseId, String month, LocalDate dueDate, + BigDecimal rent, BigDecimal charges, LocalDate paidAt) { + this.id = id; + this.landlordId = landlordId; + this.leaseId = leaseId; + this.month = month; + this.dueDate = dueDate; + this.rent = rent; + this.charges = charges; + this.paidAt = paidAt; + } + + public RentPaymentStatus statusOn(LocalDate date) { + if (paidAt != null) return RentPaymentStatus.PAID; + return dueDate.isBefore(date) ? RentPaymentStatus.LATE : RentPaymentStatus.UPCOMING; + } + + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public String getLandlordId() { return landlordId; } + public void setLandlordId(String landlordId) { this.landlordId = landlordId; } + public String getLeaseId() { return leaseId; } + public void setLeaseId(String leaseId) { this.leaseId = leaseId; } + public String getMonth() { return month; } + public void setMonth(String month) { this.month = month; } + public LocalDate getDueDate() { return dueDate; } + public void setDueDate(LocalDate dueDate) { this.dueDate = dueDate; } + public BigDecimal getRent() { return rent; } + public void setRent(BigDecimal rent) { this.rent = rent; } + public BigDecimal getCharges() { return charges; } + public void setCharges(BigDecimal charges) { this.charges = charges; } + public LocalDate getPaidAt() { return paidAt; } + public void setPaidAt(LocalDate paidAt) { this.paidAt = paidAt; } +} diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/model/RentPaymentStatus.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/model/RentPaymentStatus.java new file mode 100644 index 0000000..2aab4cd --- /dev/null +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/model/RentPaymentStatus.java @@ -0,0 +1,7 @@ +package fr.nessar.lmnporganizer.backend.model; + +public enum RentPaymentStatus { + PAID, + LATE, + UPCOMING +} diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/repository/LeaseRepository.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/repository/LeaseRepository.java index f0ebfeb..4b4c8de 100644 --- a/backend/src/main/java/fr/nessar/lmnporganizer/backend/repository/LeaseRepository.java +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/repository/LeaseRepository.java @@ -1,9 +1,13 @@ package fr.nessar.lmnporganizer.backend.repository; import fr.nessar.lmnporganizer.backend.model.Lease; +import fr.nessar.lmnporganizer.backend.model.LeaseStatus; import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface LeaseRepository extends MongoRepository { + List findAllByStatus(LeaseStatus status); } diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/repository/RentPaymentRepository.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/repository/RentPaymentRepository.java new file mode 100644 index 0000000..64f0088 --- /dev/null +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/repository/RentPaymentRepository.java @@ -0,0 +1,14 @@ +package fr.nessar.lmnporganizer.backend.repository; + +import fr.nessar.lmnporganizer.backend.model.RentPayment; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface RentPaymentRepository extends MongoRepository { + List findAllByLeaseIdAndMonthBetween(String leaseId, String firstMonth, String lastMonth); + + List findAllByOrderByDueDateDesc(); +} diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/service/RentPaymentService.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/service/RentPaymentService.java new file mode 100644 index 0000000..110e721 --- /dev/null +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/service/RentPaymentService.java @@ -0,0 +1,125 @@ +package fr.nessar.lmnporganizer.backend.service; + +import fr.nessar.lmnporganizer.backend.dto.RentPaymentResponse; +import fr.nessar.lmnporganizer.backend.model.Apartment; +import fr.nessar.lmnporganizer.backend.model.Landlord; +import fr.nessar.lmnporganizer.backend.model.Lease; +import fr.nessar.lmnporganizer.backend.model.LeaseStatus; +import fr.nessar.lmnporganizer.backend.model.RentPayment; +import fr.nessar.lmnporganizer.backend.model.Tenant; +import fr.nessar.lmnporganizer.backend.repository.ApartmentRepository; +import fr.nessar.lmnporganizer.backend.repository.LeaseRepository; +import fr.nessar.lmnporganizer.backend.repository.LandlordRepository; +import fr.nessar.lmnporganizer.backend.repository.RentPaymentRepository; +import fr.nessar.lmnporganizer.backend.repository.TenantRepository; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.YearMonth; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Service +public class RentPaymentService { + private final RentPaymentRepository rentPaymentRepository; + private final LeaseRepository leaseRepository; + private final ApartmentRepository apartmentRepository; + private final LandlordRepository landlordRepository; + private final TenantRepository tenantRepository; + private final RentReceiptPdfService rentReceiptPdfService; + + public RentPaymentService(RentPaymentRepository rentPaymentRepository, LeaseRepository leaseRepository, + ApartmentRepository apartmentRepository, LandlordRepository landlordRepository, + TenantRepository tenantRepository, + RentReceiptPdfService rentReceiptPdfService) { + this.rentPaymentRepository = rentPaymentRepository; + this.leaseRepository = leaseRepository; + this.apartmentRepository = apartmentRepository; + this.landlordRepository = landlordRepository; + this.tenantRepository = tenantRepository; + this.rentReceiptPdfService = rentReceiptPdfService; + } + + public List getAll() { + generateForActiveLeases(); + return rentPaymentRepository.findAllByOrderByDueDateDesc().stream().map(this::toResponse).toList(); + } + + public ResponseEntity markPaid(String id) { + return rentPaymentRepository.findById(id).map(payment -> { + if (payment.getPaidAt() == null) { + payment.setPaidAt(LocalDate.now()); + payment = rentPaymentRepository.save(payment); + } + return ResponseEntity.ok(toResponse(payment)); + }).orElse(ResponseEntity.notFound().build()); + } + + public ResponseEntity downloadReceipt(String id) { + RentPayment payment = rentPaymentRepository.findById(id).orElse(null); + if (payment == null) return ResponseEntity.notFound().build(); + if (payment.getPaidAt() == null) return ResponseEntity.status(HttpStatus.CONFLICT).build(); + + Lease lease = leaseRepository.findById(payment.getLeaseId()).orElse(null); + if (lease == null) return ResponseEntity.notFound().build(); + Apartment apartment = apartmentRepository.findById(lease.getApartmentId()).orElse(null); + Landlord landlord = landlordRepository.findById(payment.getLandlordId()).orElse(null); + List tenants = tenantsFor(lease.getTenantIds()); + if (apartment == null || landlord == null || tenants.size() != new HashSet<>(lease.getTenantIds()).size()) { + return ResponseEntity.notFound().build(); + } + + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_PDF) + .header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment() + .filename("quittance-" + payment.getMonth() + ".pdf").build().toString()) + .body(rentReceiptPdfService.generate(payment, lease, apartment, landlord, tenants)); + } + + private void generateForActiveLeases() { + leaseRepository.findAllByStatus(LeaseStatus.ACTIVE).forEach(this::generateForLease); + } + + private void generateForLease(Lease lease) { + if (lease.getStartDate() == null || lease.getEndDate() == null || lease.getMonthlyRent() == null + || lease.getMonthlyCharges() == null + || lease.getEndDate().isBefore(lease.getStartDate())) return; + + YearMonth firstMonth = YearMonth.from(lease.getStartDate()); + YearMonth lastMonth = YearMonth.from(lease.getEndDate()); + Set existingMonths = new HashSet<>(rentPaymentRepository + .findAllByLeaseIdAndMonthBetween(lease.getId(), firstMonth.toString(), lastMonth.toString()) + .stream().map(RentPayment::getMonth).toList()); + List missingPayments = new ArrayList<>(); + for (YearMonth month = firstMonth; !month.isAfter(lastMonth); month = month.plusMonths(1)) { + if (!existingMonths.contains(month.toString())) { + missingPayments.add(new RentPayment(null, lease.getLandlordId(), lease.getId(), month.toString(), + dueDate(lease, month), lease.getMonthlyRent(), lease.getMonthlyCharges(), null)); + } + } + if (!missingPayments.isEmpty()) rentPaymentRepository.saveAll(missingPayments); + } + + private LocalDate dueDate(Lease lease, YearMonth month) { + return month.atDay(Math.min(lease.getStartDate().getDayOfMonth(), month.lengthOfMonth())); + } + + private RentPaymentResponse toResponse(RentPayment payment) { + return new RentPaymentResponse(payment.getId(), payment.getLeaseId(), payment.getMonth(), payment.getDueDate(), + payment.getRent(), payment.getCharges(), payment.getRent().add(payment.getCharges()), + payment.statusOn(LocalDate.now()), payment.getPaidAt()); + } + + private List tenantsFor(List ids) { + List tenants = new ArrayList<>(); + tenantRepository.findAllById(ids).forEach(tenants::add); + return tenants; + } +} diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/service/RentReceiptPdfService.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/service/RentReceiptPdfService.java new file mode 100644 index 0000000..edae7ee --- /dev/null +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/service/RentReceiptPdfService.java @@ -0,0 +1,55 @@ +package fr.nessar.lmnporganizer.backend.service; + +import com.openhtmltopdf.pdfboxout.PdfRendererBuilder; +import fr.nessar.lmnporganizer.backend.model.Apartment; +import fr.nessar.lmnporganizer.backend.model.Lease; +import fr.nessar.lmnporganizer.backend.model.Landlord; +import fr.nessar.lmnporganizer.backend.model.RentPayment; +import fr.nessar.lmnporganizer.backend.model.Tenant; +import org.springframework.stereotype.Service; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Locale; + +@Service +public class RentReceiptPdfService { + private final TemplateEngine templateEngine; + + public RentReceiptPdfService(TemplateEngine templateEngine) { + this.templateEngine = templateEngine; + } + + public byte[] generate(RentPayment payment, Lease lease, Apartment apartment, Landlord landlord, + List tenants) { + Context context = new Context(Locale.FRANCE); + context.setVariable("payment", payment); + context.setVariable("lease", lease); + context.setVariable("apartment", apartment); + context.setVariable("landlord", landlord); + context.setVariable("tenantNames", namesOf(tenants)); + context.setVariable("paymentMonth", YearMonth.parse(payment.getMonth()) + .format(DateTimeFormatter.ofPattern("MMMM yyyy", Locale.FRANCE))); + context.setVariable("total", payment.getRent().add(payment.getCharges())); + + try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { + new PdfRendererBuilder().withHtmlContent(templateEngine.process("rent-receipt", context), null) + .toStream(output).run(); + return output.toByteArray(); + } catch (IOException exception) { + throw new IllegalStateException("Rent receipt PDF could not be generated", exception); + } + } + + private String namesOf(List tenants) { + List names = tenants.stream() + .map(tenant -> tenant.getFirstName() + " " + tenant.getLastName()) + .toList(); + return names.isEmpty() ? "Aucun" : String.join(", ", names); + } +} diff --git a/backend/src/main/resources/templates/rent-receipt.html b/backend/src/main/resources/templates/rent-receipt.html new file mode 100644 index 0000000..7f55424 --- /dev/null +++ b/backend/src/main/resources/templates/rent-receipt.html @@ -0,0 +1,31 @@ + + + + + + + +

QUITTANCE DE LOYER

+

Le bailleur reconnaît avoir reçu le paiement du loyer et des charges pour la période indiquée.

+ + + + + + + + + + +
BailleurBailleur LMNP
Adresse du bailleur123 rue de la Gestion, Paris
LogementStudio République
Adresse12 rue de la République
Locataire(s)Camille Martin
Périodejuillet 2026
Loyer850.00 EUR
Charges50.00 EUR
Total réglé900.00 EUR
+
Quittance générée par LMNP Organizer le 2026-07-17.
+ + diff --git a/backend/src/test/java/fr/nessar/lmnporganizer/backend/controller/RentPaymentControllerTest.java b/backend/src/test/java/fr/nessar/lmnporganizer/backend/controller/RentPaymentControllerTest.java new file mode 100644 index 0000000..ab1cc23 --- /dev/null +++ b/backend/src/test/java/fr/nessar/lmnporganizer/backend/controller/RentPaymentControllerTest.java @@ -0,0 +1,138 @@ +package fr.nessar.lmnporganizer.backend.controller; + +import fr.nessar.lmnporganizer.backend.model.Apartment; +import fr.nessar.lmnporganizer.backend.model.Landlord; +import fr.nessar.lmnporganizer.backend.model.Lease; +import fr.nessar.lmnporganizer.backend.model.LeaseStatus; +import fr.nessar.lmnporganizer.backend.model.RentPayment; +import fr.nessar.lmnporganizer.backend.model.Tenant; +import fr.nessar.lmnporganizer.backend.repository.ApartmentRepository; +import fr.nessar.lmnporganizer.backend.repository.LandlordRepository; +import fr.nessar.lmnporganizer.backend.repository.LeaseRepository; +import fr.nessar.lmnporganizer.backend.repository.RentPaymentRepository; +import fr.nessar.lmnporganizer.backend.repository.TenantRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oidcLogin; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest(properties = "lmnp.data-initializer.enabled=false") +@AutoConfigureMockMvc +class RentPaymentControllerTest { + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private RentPaymentRepository rentPaymentRepository; + + @MockitoBean + private LeaseRepository leaseRepository; + + @MockitoBean + private ApartmentRepository apartmentRepository; + + @MockitoBean + private LandlordRepository landlordRepository; + + @MockitoBean + private TenantRepository tenantRepository; + + @Test + void whenRentScheduleIsRequestedWithoutAuthentication_thenUnauthorized() throws Exception { + mockMvc.perform(get("/api/rent-payments")) + .andExpect(status().isUnauthorized()); + } + + @Test + void whenRentScheduleIsRequested_thenReturnsGeneratedPayments() throws Exception { + when(leaseRepository.findAllByStatus(LeaseStatus.ACTIVE)).thenReturn(List.of()); + when(rentPaymentRepository.findAllByOrderByDueDateDesc()).thenReturn(List.of(payment(null))); + + mockMvc.perform(get("/api/rent-payments").with(oidcLogin())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value("payment-1")) + .andExpect(jsonPath("$[0].status").value("LATE")); + } + + @Test + void whenRentPaymentIsMarkedPaid_thenReturnsPaidPayment() throws Exception { + when(rentPaymentRepository.findById("payment-1")).thenReturn(Optional.of(payment(null))); + when(rentPaymentRepository.save(any(RentPayment.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + mockMvc.perform(put("/api/rent-payments/payment-1/paid").with(oidcLogin())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("PAID")); + } + + @Test + void whenUnknownRentPaymentIsMarkedPaid_thenNotFound() throws Exception { + when(rentPaymentRepository.findById("missing")).thenReturn(Optional.empty()); + + mockMvc.perform(put("/api/rent-payments/missing/paid").with(oidcLogin())) + .andExpect(status().isNotFound()); + } + + @Test + void whenPaidReceiptIsRequested_thenReturnsPdfAttachment() throws Exception { + when(rentPaymentRepository.findById("payment-1")).thenReturn(Optional.of(payment(LocalDate.of(2026, 7, 2)))); + when(leaseRepository.findById("lease-1")).thenReturn(Optional.of(lease())); + when(apartmentRepository.findById("apartment-1")).thenReturn(Optional.of(apartment())); + when(landlordRepository.findById("default-landlord")).thenReturn(Optional.of(landlord())); + when(tenantRepository.findAllById(List.of("tenant-1"))).thenReturn(List.of(tenant())); + + mockMvc.perform(get("/api/rent-payments/payment-1/receipt").with(oidcLogin())) + .andExpect(status().isOk()) + .andExpect(header().string("Content-Type", "application/pdf")) + .andExpect(header().string("Content-Disposition", org.hamcrest.Matchers.containsString("quittance-2026-07.pdf"))); + } + + @Test + void whenUnpaidReceiptIsRequested_thenConflict() throws Exception { + when(rentPaymentRepository.findById("payment-1")).thenReturn(Optional.of(payment(null))); + + mockMvc.perform(get("/api/rent-payments/payment-1/receipt").with(oidcLogin())) + .andExpect(status().isConflict()); + } + + private RentPayment payment(LocalDate paidAt) { + return new RentPayment("payment-1", "default-landlord", "lease-1", "2026-07", + LocalDate.of(2026, 7, 1), new BigDecimal("850.00"), new BigDecimal("50.00"), paidAt); + } + + private Lease lease() { + return new Lease("lease-1", "default-landlord", "apartment-1", List.of("tenant-1"), List.of(), + LocalDate.of(2026, 1, 1), LocalDate.of(2026, 12, 31), new BigDecimal("850.00"), + new BigDecimal("50.00"), new BigDecimal("850.00"), LeaseStatus.ACTIVE); + } + + private Apartment apartment() { + return new Apartment("apartment-1", "default-landlord", "Studio République", "12 rue de la République", + 150000, ""); + } + + private Landlord landlord() { + return new Landlord("default-landlord", "Bailleur", "LMNP", "bailleur@example.com", "0102030405", + "123 rue de la Gestion, Paris"); + } + + private Tenant tenant() { + return new Tenant("tenant-1", "default-landlord", "Camille", "Martin", "camille@example.com", + "+33612345678", "10 rue des Lilas"); + } +} diff --git a/backend/src/test/java/fr/nessar/lmnporganizer/backend/model/ModelCoverageTest.java b/backend/src/test/java/fr/nessar/lmnporganizer/backend/model/ModelCoverageTest.java index 02a2473..d44704c 100644 --- a/backend/src/test/java/fr/nessar/lmnporganizer/backend/model/ModelCoverageTest.java +++ b/backend/src/test/java/fr/nessar/lmnporganizer/backend/model/ModelCoverageTest.java @@ -192,4 +192,34 @@ class ModelCoverageTest { BigDecimal.ZERO, BigDecimal.TEN, LeaseStatus.DRAFT); assertEquals("lease-2", fullLease.getId()); } + + @Test + void testRentPaymentAccessorsAndStatus() { + RentPayment payment = new RentPayment(); + payment.setId("payment-1"); + payment.setLandlordId("default-landlord"); + payment.setLeaseId("lease-1"); + payment.setMonth("2026-07"); + payment.setDueDate(LocalDate.of(2026, 7, 1)); + payment.setRent(new BigDecimal("850")); + payment.setCharges(new BigDecimal("50")); + + assertEquals("payment-1", payment.getId()); + assertEquals("default-landlord", payment.getLandlordId()); + assertEquals("lease-1", payment.getLeaseId()); + assertEquals("2026-07", payment.getMonth()); + assertEquals(LocalDate.of(2026, 7, 1), payment.getDueDate()); + assertEquals(new BigDecimal("850"), payment.getRent()); + assertEquals(new BigDecimal("50"), payment.getCharges()); + assertEquals(RentPaymentStatus.LATE, payment.statusOn(LocalDate.of(2026, 7, 2))); + assertEquals(RentPaymentStatus.UPCOMING, payment.statusOn(LocalDate.of(2026, 7, 1))); + + payment.setPaidAt(LocalDate.of(2026, 7, 3)); + assertEquals(LocalDate.of(2026, 7, 3), payment.getPaidAt()); + assertEquals(RentPaymentStatus.PAID, payment.statusOn(LocalDate.of(2026, 7, 2))); + + RentPayment fullPayment = new RentPayment("payment-2", "landlord", "lease-2", "2026-08", + LocalDate.of(2026, 8, 1), BigDecimal.ONE, BigDecimal.ZERO, null); + assertEquals("payment-2", fullPayment.getId()); + } } diff --git a/backend/src/test/java/fr/nessar/lmnporganizer/backend/service/RentPaymentServiceTest.java b/backend/src/test/java/fr/nessar/lmnporganizer/backend/service/RentPaymentServiceTest.java new file mode 100644 index 0000000..a977973 --- /dev/null +++ b/backend/src/test/java/fr/nessar/lmnporganizer/backend/service/RentPaymentServiceTest.java @@ -0,0 +1,164 @@ +package fr.nessar.lmnporganizer.backend.service; + +import fr.nessar.lmnporganizer.backend.dto.RentPaymentResponse; +import fr.nessar.lmnporganizer.backend.model.Apartment; +import fr.nessar.lmnporganizer.backend.model.Lease; +import fr.nessar.lmnporganizer.backend.model.LeaseStatus; +import fr.nessar.lmnporganizer.backend.model.Landlord; +import fr.nessar.lmnporganizer.backend.model.RentPayment; +import fr.nessar.lmnporganizer.backend.model.RentPaymentStatus; +import fr.nessar.lmnporganizer.backend.model.Tenant; +import fr.nessar.lmnporganizer.backend.repository.ApartmentRepository; +import fr.nessar.lmnporganizer.backend.repository.LeaseRepository; +import fr.nessar.lmnporganizer.backend.repository.LandlordRepository; +import fr.nessar.lmnporganizer.backend.repository.RentPaymentRepository; +import fr.nessar.lmnporganizer.backend.repository.TenantRepository; +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.text.PDFTextStripper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.thymeleaf.spring6.SpringTemplateEngine; +import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; +import java.util.stream.StreamSupport; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class RentPaymentServiceTest { + @Mock + private RentPaymentRepository rentPaymentRepository; + + @Mock + private LeaseRepository leaseRepository; + + @Mock + private ApartmentRepository apartmentRepository; + + @Mock + private LandlordRepository landlordRepository; + + @Mock + private TenantRepository tenantRepository; + + private RentPaymentService service; + + @BeforeEach + void setUp() { + ClassLoaderTemplateResolver resolver = new ClassLoaderTemplateResolver(); + resolver.setPrefix("templates/"); + resolver.setSuffix(".html"); + SpringTemplateEngine templateEngine = new SpringTemplateEngine(); + templateEngine.setTemplateResolver(resolver); + service = new RentPaymentService(rentPaymentRepository, leaseRepository, apartmentRepository, + landlordRepository, tenantRepository, new RentReceiptPdfService(templateEngine)); + } + + @Test + void generatesARecordForEachMonthOfAnActiveLease() { + Lease lease = lease(LocalDate.of(2099, 1, 31), LocalDate.of(2099, 3, 31)); + when(leaseRepository.findAllByStatus(LeaseStatus.ACTIVE)).thenReturn(List.of(lease)); + when(rentPaymentRepository.findAllByLeaseIdAndMonthBetween("lease-1", "2099-01", "2099-03")) + .thenReturn(List.of()); + when(rentPaymentRepository.findAllByOrderByDueDateDesc()).thenReturn(List.of( + payment("payment-3", "2099-03", LocalDate.of(2099, 3, 31), null), + payment("payment-2", "2099-02", LocalDate.of(2099, 2, 28), null), + payment("payment-1", "2099-01", LocalDate.of(2099, 1, 31), null))); + + List payments = service.getAll(); + + assertEquals(3, payments.size()); + assertEquals("2099-03", payments.getFirst().month()); + assertEquals(new BigDecimal("900.00"), payments.getFirst().total()); + assertEquals(RentPaymentStatus.UPCOMING, payments.getFirst().status()); + verify(rentPaymentRepository).saveAll(argThat(generatedPayments -> { + List records = StreamSupport.stream(generatedPayments.spliterator(), false).toList(); + return records.stream().map(RentPayment::getMonth).toList().equals(List.of("2099-01", "2099-02", "2099-03")) + && records.get(1).getDueDate().equals(LocalDate.of(2099, 2, 28)); + })); + } + + @Test + void marksAnUnpaidRentPaymentPaidOnlyOnce() { + RentPayment payment = payment("payment-1", "2000-01", LocalDate.of(2000, 1, 1), null); + when(rentPaymentRepository.findById("payment-1")).thenReturn(Optional.of(payment)); + when(rentPaymentRepository.save(any(RentPayment.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + ResponseEntity response = service.markPaid("payment-1"); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); + assertEquals(RentPaymentStatus.PAID, response.getBody().status()); + assertNotNull(response.getBody().paidAt()); + verify(rentPaymentRepository).save(payment); + + service.markPaid("payment-1"); + verify(rentPaymentRepository, times(1)).save(payment); + } + + @Test + void rendersAPdfReceiptOnlyAfterPayment() throws Exception { + RentPayment payment = payment("payment-1", "2026-07", LocalDate.of(2026, 7, 1), LocalDate.of(2026, 7, 2)); + when(rentPaymentRepository.findById("payment-1")).thenReturn(Optional.of(payment)); + when(leaseRepository.findById("lease-1")).thenReturn(Optional.of(lease( + LocalDate.of(2026, 1, 1), LocalDate.of(2026, 12, 31)))); + when(apartmentRepository.findById("apartment-1")).thenReturn(Optional.of(apartment())); + when(landlordRepository.findById("default-landlord")).thenReturn(Optional.of(landlord())); + when(tenantRepository.findAllById(List.of("tenant-1"))).thenReturn(List.of(tenant())); + + ResponseEntity response = service.downloadReceipt("payment-1"); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals("application/pdf", response.getHeaders().getContentType().toString()); + assertTrue(response.getHeaders().getFirst("Content-Disposition").contains("quittance-2026-07.pdf")); + try (PDDocument document = Loader.loadPDF(response.getBody())) { + String pdfText = new PDFTextStripper().getText(document); + assertTrue(pdfText.contains("QUITTANCE DE LOYER")); + assertTrue(pdfText.contains("Studio République")); + assertTrue(pdfText.contains("Camille Martin")); + assertTrue(pdfText.contains("Bailleur LMNP")); + } + + RentPayment unpaid = payment("payment-2", "2026-08", LocalDate.of(2026, 8, 1), null); + when(rentPaymentRepository.findById("payment-2")).thenReturn(Optional.of(unpaid)); + assertEquals(HttpStatus.CONFLICT, service.downloadReceipt("payment-2").getStatusCode()); + } + + private Apartment apartment() { + return new Apartment("apartment-1", "default-landlord", "Studio République", "12 rue de la République", + 150000, ""); + } + + private Tenant tenant() { + return new Tenant("tenant-1", "default-landlord", "Camille", "Martin", "camille@example.com", + "+33612345678", "10 rue des Lilas"); + } + + private Landlord landlord() { + return new Landlord("default-landlord", "Bailleur", "LMNP", "bailleur@example.com", "0102030405", + "123 rue de la Gestion, Paris"); + } + + private Lease lease(LocalDate startDate, LocalDate endDate) { + return new Lease("lease-1", "default-landlord", "apartment-1", List.of("tenant-1"), List.of(), startDate, + endDate, new BigDecimal("850.00"), new BigDecimal("50.00"), new BigDecimal("850.00"), + LeaseStatus.ACTIVE); + } + + private RentPayment payment(String id, String month, LocalDate dueDate, LocalDate paidAt) { + return new RentPayment(id, "default-landlord", "lease-1", month, dueDate, new BigDecimal("850.00"), + new BigDecimal("50.00"), paidAt); + } +} diff --git a/frontend/src/app/app.routes.spec.ts b/frontend/src/app/app.routes.spec.ts index f591d53..68feaf9 100644 --- a/frontend/src/app/app.routes.spec.ts +++ b/frontend/src/app/app.routes.spec.ts @@ -8,6 +8,7 @@ describe('routes', () => { 'apartments', 'tenants', 'leases', + 'rent-payments', 'settings', '', '**', diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index d764a52..95b8d5d 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -2,6 +2,7 @@ import { Routes } from '@angular/router'; import { ApartmentsPage } from './components/apartments-page/apartments-page'; import { DashboardPage } from './components/dashboard-page/dashboard-page'; import { LeasesPage } from './components/leases-page/leases-page'; +import { RentPaymentsPage } from './components/rent-payments-page/rent-payments-page'; import { SettingsPage } from './components/settings-page/settings-page'; import { TenantsPage } from './components/tenants-page/tenants-page'; @@ -10,6 +11,7 @@ export const routes: Routes = [ { path: 'apartments', component: ApartmentsPage }, { path: 'tenants', component: TenantsPage }, { path: 'leases', component: LeasesPage }, + { path: 'rent-payments', component: RentPaymentsPage }, { path: 'settings', component: SettingsPage }, { path: '', pathMatch: 'full', redirectTo: 'dashboard' }, { path: '**', redirectTo: 'dashboard' }, diff --git a/frontend/src/app/components/rent-payments-page/rent-payments-page.css b/frontend/src/app/components/rent-payments-page/rent-payments-page.css new file mode 100644 index 0000000..e6c5cce --- /dev/null +++ b/frontend/src/app/components/rent-payments-page/rent-payments-page.css @@ -0,0 +1,63 @@ +.rent-payment-grid { + display: grid; + gap: var(--space-4, 1rem); +} + +.rent-payment { + padding: var(--space-5, 1.25rem); + border: 1px solid var(--border, #dce5df); + border-radius: var(--radius-xl, 1.375rem); + background: var(--surface, #ffffff); +} + +.payment-topline, +.rent-payment dl > div { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3, 0.75rem); +} + +.payment-topline { + margin-bottom: var(--space-4, 1rem); +} + +.rent-payment h2 { + margin-bottom: var(--space-1, 0.25rem); +} + +.payment-month, +.rent-payment dt { + color: var(--text-muted, #687670); +} + +.rent-payment dl { + margin: var(--space-5, 1.25rem) 0; +} + +.rent-payment dl > div { + padding: var(--space-2, 0.5rem) 0; + border-bottom: 1px solid var(--border, #dce5df); +} + +.rent-payment dd { + margin: 0; + font-weight: 700; +} + +.payment-status { + font-size: 0.8rem; + font-weight: 700; +} + +.payment-status.paid { color: var(--accent, #0f766e); } +.payment-status.late { color: var(--danger, #b42318); } +.payment-status.upcoming { color: var(--text-muted, #687670); } + +@media (min-width: 700px) { + .rent-payment-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} + +@media (min-width: 1100px) { + .rent-payment-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } +} diff --git a/frontend/src/app/components/rent-payments-page/rent-payments-page.html b/frontend/src/app/components/rent-payments-page/rent-payments-page.html new file mode 100644 index 0000000..ad883c7 --- /dev/null +++ b/frontend/src/app/components/rent-payments-page/rent-payments-page.html @@ -0,0 +1,100 @@ +
+
+
+

Location

+

Mes loyers

+

Suivez les échéances et téléchargez les quittances après règlement.

+
+
+ +
+ @if (!contentReady() && loadError()) { + + } @else if (!contentReady()) { +
+ @for (placeholder of [1, 2, 3]; track placeholder) { + + } +
+ } @else { + @if (contentLoading()) { + + } + @if (loadError()) { + + } + @if (workspace.rentPayments().length === 0) { +
+

Échéancier

+

Aucune échéance de loyer

+

Les échéances sont créées automatiquement pour les baux actifs.

+
+ } @else { +
+ @for (payment of workspace.rentPayments(); track payment.id) { +
+
+ + {{ statusLabel(payment.status) }} + + {{ payment.total.toLocaleString('fr-FR') }} € +
+

{{ apartmentName(payment) }}

+

{{ monthLabel(payment.month) }}

+
+
Loyer
{{ payment.rent.toLocaleString('fr-FR') }} €
+
Charges
{{ payment.charges.toLocaleString('fr-FR') }} €
+
Échéance
{{ payment.dueDate }}
+
+ @if (workspace.rentPaymentSaveError(payment.id) || workspace.rentReceiptDownloadError(payment.id)) { + + } +
+ @if (payment.status === 'PAID') { + + } @else { + + } +
+
+ } +
+ } + } +
+
diff --git a/frontend/src/app/components/rent-payments-page/rent-payments-page.spec.ts b/frontend/src/app/components/rent-payments-page/rent-payments-page.spec.ts new file mode 100644 index 0000000..93aa3b1 --- /dev/null +++ b/frontend/src/app/components/rent-payments-page/rent-payments-page.spec.ts @@ -0,0 +1,49 @@ +import { signal } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { WorkspaceService } from '../../services/workspace.service'; +import { RentPaymentsPage } from './rent-payments-page'; + +describe('RentPaymentsPage', () => { + it('loads the schedule and exposes payment actions after its dependencies are ready', () => { + const workspace = { + apartments: signal([{ id: 'apartment-1', name: 'T2' }]), + leases: signal([{ id: 'lease-1', apartmentId: 'apartment-1' }]), + rentPayments: signal([ + { + id: 'payment-1', + leaseId: 'lease-1', + month: '2026-07', + dueDate: '2026-07-01', + rent: 800, + charges: 50, + total: 850, + status: 'LATE', + paidAt: null, + }, + ]), + apartmentLoadState: signal({ loaded: true, loading: false, error: '' }), + leaseLoadState: signal({ loaded: true, loading: false, error: '' }), + rentPaymentLoadState: signal({ loaded: true, loading: false, error: '' }), + loadApartments: vi.fn(), + loadLeases: vi.fn(), + loadRentPayments: vi.fn(), + markRentPaymentPaid: vi.fn(), + rentPaymentSaving: vi.fn(() => false), + rentPaymentSaveError: vi.fn(() => ''), + downloadRentReceipt: vi.fn(), + rentReceiptDownloading: vi.fn(() => false), + rentReceiptDownloadError: vi.fn(() => ''), + }; + TestBed.configureTestingModule({ + providers: [{ provide: WorkspaceService, useValue: workspace }], + }); + + const fixture = TestBed.createComponent(RentPaymentsPage); + fixture.detectChanges(); + + expect(workspace.loadRentPayments).toHaveBeenCalled(); + expect(fixture.nativeElement.textContent).toContain('T2'); + fixture.nativeElement.querySelector('button').click(); + expect(workspace.markRentPaymentPaid).toHaveBeenCalledWith('payment-1'); + }); +}); diff --git a/frontend/src/app/components/rent-payments-page/rent-payments-page.ts b/frontend/src/app/components/rent-payments-page/rent-payments-page.ts new file mode 100644 index 0000000..1779ad9 --- /dev/null +++ b/frontend/src/app/components/rent-payments-page/rent-payments-page.ts @@ -0,0 +1,59 @@ +import { Component, computed, inject } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { RentPayment, RentPaymentStatus } from '../../services/rent-payment.service'; +import { WorkspaceService } from '../../services/workspace.service'; + +@Component({ + selector: 'app-rent-payments-page', + imports: [MatButtonModule, MatProgressBarModule, MatProgressSpinnerModule], + templateUrl: './rent-payments-page.html', + styleUrl: './rent-payments-page.css', +}) +export class RentPaymentsPage { + readonly workspace = inject(WorkspaceService); + readonly contentReady = computed( + () => + this.workspace.rentPaymentLoadState().loaded && + this.workspace.leaseLoadState().loaded && + this.workspace.apartmentLoadState().loaded, + ); + readonly contentLoading = computed( + () => + this.workspace.rentPaymentLoadState().loading || + this.workspace.leaseLoadState().loading || + this.workspace.apartmentLoadState().loading, + ); + readonly loadError = computed( + () => + this.workspace.rentPaymentLoadState().error || + this.workspace.leaseLoadState().error || + this.workspace.apartmentLoadState().error, + ); + + constructor() { + this.load(); + } + + apartmentName(payment: RentPayment): string { + const lease = this.workspace.leases().find((item) => item.id === payment.leaseId); + return this.workspace.apartments().find((item) => item.id === lease?.apartmentId)?.name ?? 'Bien inconnu'; + } + + monthLabel(month: string): string { + return new Intl.DateTimeFormat('fr-FR', { month: 'long', year: 'numeric' }).format( + new Date(`${month}-01T12:00:00`), + ); + } + + statusLabel(status: RentPaymentStatus): string { + return { PAID: 'Payé', LATE: 'En retard', UPCOMING: 'À venir' }[status]; + } + + load(): void { + this.workspace.loadRentPayments(); + this.workspace.loadLeases(); + this.workspace.loadApartments(); + } +} diff --git a/frontend/src/app/components/workspace-navigation/workspace-navigation.html b/frontend/src/app/components/workspace-navigation/workspace-navigation.html index d69ee89..7559d1e 100644 --- a/frontend/src/app/components/workspace-navigation/workspace-navigation.html +++ b/frontend/src/app/components/workspace-navigation/workspace-navigation.html @@ -45,6 +45,16 @@