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

+9
View File
@@ -50,6 +50,15 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>io.github.openhtmltopdf</groupId>
<artifactId>openhtmltopdf-pdfbox</artifactId>
<version>1.1.40</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -1,91 +1,48 @@
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.Tenant;
import org.springframework.stereotype.Service;
import org.springframework.core.io.ClassPathResource;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@Service
public class LeasePdfService {
private final TemplateEngine templateEngine;
public LeasePdfService(TemplateEngine templateEngine) {
this.templateEngine = templateEngine;
}
public byte[] generate(Lease lease, Apartment apartment, List<Tenant> tenants) {
String tenantNames = tenants.stream().map(tenant -> tenant.getFirstName() + " " + tenant.getLastName())
.reduce((first, second) -> first + ", " + second).orElse("");
String guarantorNames = lease.getGuarantors().stream()
.map(guarantor -> guarantor.getFirstName() + " " + guarantor.getLastName())
.reduce((first, second) -> first + ", " + second).orElse("Aucun");
String renderedTemplate = loadTemplate()
.replace("{{leaseId}}", lease.getId())
.replace("{{apartmentName}}", apartment.getName())
.replace("{{apartmentAddress}}", apartment.getAddress())
.replace("{{tenantNames}}", tenantNames)
.replace("{{startDate}}", String.valueOf(lease.getStartDate()))
.replace("{{endDate}}", String.valueOf(lease.getEndDate()))
.replace("{{monthlyRent}}", String.valueOf(lease.getMonthlyRent()))
.replace("{{monthlyCharges}}", String.valueOf(lease.getMonthlyCharges()))
.replace("{{securityDeposit}}", String.valueOf(lease.getSecurityDeposit()))
.replace("{{guarantorNames}}", guarantorNames);
return buildPdf(List.of(renderedTemplate.split("\\R", -1)));
}
Context context = new Context(Locale.FRANCE);
context.setVariable("lease", lease);
context.setVariable("apartment", apartment);
context.setVariable("tenantNames", tenantNamesOf(tenants));
context.setVariable("guarantorNames", namesOf(lease.getGuarantors().stream()
.map(guarantor -> guarantor.getFirstName() + " " + guarantor.getLastName()).toList()));
private String loadTemplate() {
try {
return new ClassPathResource("templates/lease.txt").getContentAsString(StandardCharsets.UTF_8);
try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
new PdfRendererBuilder().withHtmlContent(templateEngine.process("lease", context), null)
.toStream(output).run();
return output.toByteArray();
} catch (IOException exception) {
throw new IllegalStateException("Lease PDF template is unavailable", exception);
throw new IllegalStateException("Lease PDF could not be generated", exception);
}
}
private byte[] buildPdf(List<String> lines) {
StringBuilder stream = new StringBuilder("BT\n/F1 18 Tf\n50 790 Td\n");
for (int index = 0; index < lines.size(); index++) {
if (index == 1) stream.append("/F1 11 Tf\n");
stream.append('(').append(escape(lines.get(index))).append(") Tj\n0 -28 Td\n");
}
stream.append("ET\n");
byte[] streamBytes = stream.toString().getBytes(StandardCharsets.US_ASCII);
List<byte[]> objects = List.of(
ascii("<< /Type /Catalog /Pages 2 0 R >>"),
ascii("<< /Type /Pages /Kids [3 0 R] /Count 1 >>"),
ascii("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>"),
ascii("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>"),
concat(ascii("<< /Length " + streamBytes.length + " >>\nstream\n"), streamBytes, ascii("endstream")));
ByteArrayOutputStream output = new ByteArrayOutputStream();
write(output, ascii("%PDF-1.4\n"));
List<Integer> offsets = new ArrayList<>();
for (int index = 0; index < objects.size(); index++) {
offsets.add(output.size());
write(output, ascii((index + 1) + " 0 obj\n"));
write(output, objects.get(index));
write(output, ascii("\nendobj\n"));
}
int xrefOffset = output.size();
write(output, ascii("xref\n0 " + (objects.size() + 1) + "\n0000000000 65535 f \n"));
offsets.forEach(offset -> write(output, ascii(String.format(Locale.ROOT, "%010d 00000 n \n", offset))));
write(output, ascii("trailer\n<< /Size " + (objects.size() + 1) + " /Root 1 0 R >>\nstartxref\n"
+ xrefOffset + "\n%%EOF\n"));
return output.toByteArray();
private String tenantNamesOf(List<Tenant> tenants) {
return namesOf(tenants.stream().map(tenant -> tenant.getFirstName() + " " + tenant.getLastName()).toList());
}
private String escape(String value) {
String ascii = Normalizer.normalize(String.valueOf(value), Normalizer.Form.NFD)
.replaceAll("\\p{M}", "").replaceAll("[^\\x20-\\x7E]", "?");
return ascii.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)");
private String namesOf(List<String> names) {
return names.isEmpty() ? "Aucun" : String.join(", ", names);
}
private byte[] ascii(String value) { return value.getBytes(StandardCharsets.US_ASCII); }
private byte[] concat(byte[]... parts) {
ByteArrayOutputStream output = new ByteArrayOutputStream();
for (byte[] part : parts) write(output, part);
return output.toByteArray();
}
private void write(ByteArrayOutputStream output, byte[] bytes) { output.writeBytes(bytes); }
}
@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="fr" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<style>
@page { size: A4; margin: 2cm; }
body { color: #1f2937; font-family: serif; font-size: 11pt; line-height: 1.5; }
h1 { color: #0f766e; font-size: 22pt; margin: 0 0 0.5cm; }
.reference { color: #4b5563; margin-bottom: 1cm; }
table { border-collapse: collapse; width: 100%; }
th, td { border-bottom: 1px solid #d1d5db; padding: 0.25cm 0; text-align: left; vertical-align: top; }
th { width: 35%; font-weight: bold; }
footer { color: #4b5563; font-size: 9pt; margin-top: 1cm; }
</style>
</head>
<body>
<h1>BAIL D'HABITATION MEUBLÉE</h1>
<p class="reference">Référence : <span th:text="${lease.id}">lease-1</span></p>
<table>
<tr><th>Logement</th><td th:text="${apartment.name}">Studio République</td></tr>
<tr><th>Adresse</th><td th:text="${apartment.address}">12 rue de la République</td></tr>
<tr><th>Locataire(s)</th><td th:text="${tenantNames}">Camille Martin</td></tr>
<tr><th>Période</th><td><span th:text="${lease.startDate}">2026-09-01</span> au <span th:text="${lease.endDate}">2027-08-31</span></td></tr>
<tr><th>Loyer mensuel</th><td><span th:text="${lease.monthlyRent}">850.00</span> EUR</td></tr>
<tr><th>Charges mensuelles</th><td><span th:text="${lease.monthlyCharges}">50.00</span> EUR</td></tr>
<tr><th>Dépôt de garantie</th><td><span th:text="${lease.securityDeposit}">850.00</span> EUR</td></tr>
<tr><th>Garant(s)</th><td th:text="${guarantorNames}">Alex Durand</td></tr>
</table>
<footer>Bail généré par LMNP Organizer. À compléter et signer par les parties.</footer>
</body>
</html>
@@ -1,12 +0,0 @@
BAIL D'HABITATION MEUBLÉE
Référence : {{leaseId}}
Logement : {{apartmentName}}
Adresse : {{apartmentAddress}}
Locataire(s) : {{tenantNames}}
Période : {{startDate}} au {{endDate}}
Loyer mensuel : {{monthlyRent}} EUR
Charges mensuelles : {{monthlyCharges}} EUR
Dépôt de garantie : {{securityDeposit}} EUR
Garant(s) : {{guarantorNames}}
Bail généré par LMNP Organizer. À compléter et signer par les parties.
@@ -8,6 +8,9 @@ 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.TenantRepository;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@@ -144,9 +147,13 @@ class LeaseControllerTest {
if (content.length < 100 || content[0] != '%' || content[1] != 'P') {
throw new AssertionError("Expected a generated lease PDF");
}
String pdf = new String(content, java.nio.charset.StandardCharsets.US_ASCII);
if (!pdf.contains("Studio Republique") || !pdf.contains("Camille Martin")) {
throw new AssertionError("Expected rendered lease template values");
try (PDDocument document = Loader.loadPDF(content)) {
String pdfText = new PDFTextStripper().getText(document);
if (!pdfText.contains("Studio République") || !pdfText.contains("Camille Martin")) {
throw new AssertionError("Expected rendered lease template values");
}
} catch (java.io.IOException exception) {
throw new AssertionError("Expected a readable PDF", exception);
}
});
}