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:
1 parent
be1f680a62
commit
82f339e209
33 files changed
+292
-929
No files matched your search
@@ -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>
|
||||
|
||||
+24
-67
@@ -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.
|
||||
+10
-3
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user