From 767b65008bdd6ea0b3cf90fd03537c75e5e8bccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Wed, 4 Mar 2026 12:15:23 +0100 Subject: [PATCH] feat(back-end and front-end) email for request --- .../CustomQuoteRequestController.java | 253 ++++++++++++++++++ .../printcalculator/dto/QuoteRequestDto.java | 1 + .../src/main/resources/application.properties | 1 + .../email/contact-request-customer.html | 134 ++++++++++ .../core/services/quote-request.service.ts | 1 + .../features/about/about-page.component.ts | 3 + .../pages/admin-cad-invoices.component.html | 17 +- .../pages/admin-cad-invoices.component.ts | 3 +- .../admin-contact-requests.component.html | 5 +- .../pages/admin-contact-requests.component.ts | 3 +- .../pages/admin-dashboard.component.html | 5 +- .../admin/pages/admin-dashboard.component.ts | 3 +- .../admin/pages/admin-sessions.component.html | 28 +- .../admin/pages/admin-sessions.component.ts | 39 +-- .../contact-form/contact-form.component.ts | 3 + .../directives/copy-on-click.directive.ts | 45 ++++ frontend/src/assets/i18n/de.json | 1 + frontend/src/assets/i18n/en.json | 1 + frontend/src/assets/i18n/fr.json | 1 + frontend/src/assets/i18n/it.json | 1 + 20 files changed, 493 insertions(+), 55 deletions(-) create mode 100644 backend/src/main/resources/templates/email/contact-request-customer.html create mode 100644 frontend/src/app/shared/directives/copy-on-click.directive.ts diff --git a/backend/src/main/java/com/printcalculator/controller/CustomQuoteRequestController.java b/backend/src/main/java/com/printcalculator/controller/CustomQuoteRequestController.java index b407bcb..003ade8 100644 --- a/backend/src/main/java/com/printcalculator/controller/CustomQuoteRequestController.java +++ b/backend/src/main/java/com/printcalculator/controller/CustomQuoteRequestController.java @@ -29,6 +29,8 @@ import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.time.OffsetDateTime; import java.time.Year; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -53,6 +55,9 @@ public class CustomQuoteRequestController { @Value("${app.mail.contact-request.admin.address:infog@3d-fab.ch}") private String contactRequestAdminMailAddress; + @Value("${app.mail.contact-request.customer.enabled:true}") + private boolean contactRequestCustomerMailEnabled; + // TODO: Inject Storage Service private static final Path STORAGE_ROOT = Paths.get("storage_requests").toAbsolutePath().normalize(); private static final Pattern SAFE_EXTENSION_PATTERN = Pattern.compile("^[a-z0-9]{1,10}$"); @@ -97,6 +102,7 @@ public class CustomQuoteRequestController { "Accettazione Termini e Privacy obbligatoria." ); } + String language = normalizeLanguage(requestDto.getLanguage()); // 1. Create Request CustomQuoteRequest request = new CustomQuoteRequest(); @@ -173,6 +179,7 @@ public class CustomQuoteRequestController { } sendAdminContactRequestNotification(request, attachmentsCount); + sendCustomerContactRequestConfirmation(request, attachmentsCount, language); return ResponseEntity.ok(request); } @@ -258,6 +265,252 @@ public class CustomQuoteRequestController { ); } + private void sendCustomerContactRequestConfirmation(CustomQuoteRequest request, int attachmentsCount, String language) { + if (!contactRequestCustomerMailEnabled) { + return; + } + if (request.getEmail() == null || request.getEmail().isBlank()) { + logger.warn("Contact request confirmation skipped: missing customer email for request {}", request.getId()); + return; + } + + Map templateData = new HashMap<>(); + templateData.put("requestId", request.getId()); + templateData.put( + "createdAt", + request.getCreatedAt().format( + DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(localeForLanguage(language)) + ) + ); + templateData.put("recipientName", resolveRecipientName(request, language)); + templateData.put("requestType", localizeRequestType(request.getRequestType(), language)); + templateData.put("customerType", localizeCustomerType(request.getCustomerType(), language)); + templateData.put("name", safeValue(request.getName())); + templateData.put("companyName", safeValue(request.getCompanyName())); + templateData.put("contactPerson", safeValue(request.getContactPerson())); + templateData.put("email", safeValue(request.getEmail())); + templateData.put("phone", safeValue(request.getPhone())); + templateData.put("message", safeValue(request.getMessage())); + templateData.put("attachmentsCount", attachmentsCount); + templateData.put("currentYear", Year.now().getValue()); + String subject = applyCustomerContactRequestTexts(templateData, language, request.getId()); + + emailNotificationService.sendEmail( + request.getEmail(), + subject, + "contact-request-customer", + templateData + ); + } + + private String applyCustomerContactRequestTexts( + Map templateData, + String language, + UUID requestId + ) { + return switch (language) { + case "en" -> { + templateData.put("emailTitle", "Contact request received"); + templateData.put("headlineText", "We received your contact request"); + templateData.put("greetingText", "Hi " + templateData.get("recipientName") + ","); + templateData.put("introText", "Thank you for contacting us. Our team will reply as soon as possible."); + templateData.put("requestIdHintText", "Please keep this request ID for future order references:"); + templateData.put("detailsTitleText", "Request details"); + templateData.put("labelRequestId", "Request ID"); + templateData.put("labelDate", "Date"); + templateData.put("labelRequestType", "Request type"); + templateData.put("labelCustomerType", "Customer type"); + templateData.put("labelName", "Name"); + templateData.put("labelCompany", "Company"); + templateData.put("labelContactPerson", "Contact person"); + templateData.put("labelEmail", "Email"); + templateData.put("labelPhone", "Phone"); + templateData.put("labelMessage", "Message"); + templateData.put("labelAttachments", "Attachments"); + templateData.put("supportText", "If you need help, reply to this email."); + templateData.put("footerText", "Automated request-receipt confirmation from 3D-Fab."); + yield "We received your contact request #" + requestId + " - 3D-Fab"; + } + case "de" -> { + templateData.put("emailTitle", "Kontaktanfrage erhalten"); + templateData.put("headlineText", "Wir haben Ihre Kontaktanfrage erhalten"); + templateData.put("greetingText", "Hallo " + templateData.get("recipientName") + ","); + templateData.put("introText", "Vielen Dank fuer Ihre Anfrage. Unser Team antwortet Ihnen so schnell wie moeglich."); + templateData.put("requestIdHintText", "Bitte speichern Sie diese Anfrage-ID fuer zukuenftige Bestellreferenzen:"); + templateData.put("detailsTitleText", "Anfragedetails"); + templateData.put("labelRequestId", "Anfrage-ID"); + templateData.put("labelDate", "Datum"); + templateData.put("labelRequestType", "Anfragetyp"); + templateData.put("labelCustomerType", "Kundentyp"); + templateData.put("labelName", "Name"); + templateData.put("labelCompany", "Firma"); + templateData.put("labelContactPerson", "Kontaktperson"); + templateData.put("labelEmail", "E-Mail"); + templateData.put("labelPhone", "Telefon"); + templateData.put("labelMessage", "Nachricht"); + templateData.put("labelAttachments", "Anhaenge"); + templateData.put("supportText", "Wenn Sie Hilfe brauchen, antworten Sie auf diese E-Mail."); + templateData.put("footerText", "Automatische Bestaetigung des Anfrageeingangs von 3D-Fab."); + yield "Wir haben Ihre Kontaktanfrage erhalten #" + requestId + " - 3D-Fab"; + } + case "fr" -> { + templateData.put("emailTitle", "Demande de contact recue"); + templateData.put("headlineText", "Nous avons recu votre demande de contact"); + templateData.put("greetingText", "Bonjour " + templateData.get("recipientName") + ","); + templateData.put("introText", "Merci pour votre message. Notre equipe vous repondra des que possible."); + templateData.put("requestIdHintText", "Veuillez conserver cet ID de demande pour vos futures references de commande :"); + templateData.put("detailsTitleText", "Details de la demande"); + templateData.put("labelRequestId", "ID de demande"); + templateData.put("labelDate", "Date"); + templateData.put("labelRequestType", "Type de demande"); + templateData.put("labelCustomerType", "Type de client"); + templateData.put("labelName", "Nom"); + templateData.put("labelCompany", "Entreprise"); + templateData.put("labelContactPerson", "Contact"); + templateData.put("labelEmail", "Email"); + templateData.put("labelPhone", "Telephone"); + templateData.put("labelMessage", "Message"); + templateData.put("labelAttachments", "Pieces jointes"); + templateData.put("supportText", "Si vous avez besoin d'aide, repondez a cet email."); + templateData.put("footerText", "Confirmation automatique de reception de demande par 3D-Fab."); + yield "Nous avons recu votre demande de contact #" + requestId + " - 3D-Fab"; + } + default -> { + templateData.put("emailTitle", "Richiesta di contatto ricevuta"); + templateData.put("headlineText", "Abbiamo ricevuto la tua richiesta di contatto"); + templateData.put("greetingText", "Ciao " + templateData.get("recipientName") + ","); + templateData.put("introText", "Grazie per averci contattato. Il nostro team ti rispondera' il prima possibile."); + templateData.put("requestIdHintText", "Conserva questo ID richiesta per i futuri riferimenti d'ordine:"); + templateData.put("detailsTitleText", "Dettagli richiesta"); + templateData.put("labelRequestId", "ID richiesta"); + templateData.put("labelDate", "Data"); + templateData.put("labelRequestType", "Tipo richiesta"); + templateData.put("labelCustomerType", "Tipo cliente"); + templateData.put("labelName", "Nome"); + templateData.put("labelCompany", "Azienda"); + templateData.put("labelContactPerson", "Contatto"); + templateData.put("labelEmail", "Email"); + templateData.put("labelPhone", "Telefono"); + templateData.put("labelMessage", "Messaggio"); + templateData.put("labelAttachments", "Allegati"); + templateData.put("supportText", "Se hai bisogno, rispondi direttamente a questa email."); + templateData.put("footerText", "Conferma automatica di ricezione richiesta da 3D-Fab."); + yield "Abbiamo ricevuto la tua richiesta di contatto #" + requestId + " - 3D-Fab"; + } + }; + } + + private String localizeRequestType(String requestType, String language) { + if (requestType == null || requestType.isBlank()) { + return "-"; + } + + String normalized = requestType.trim().toLowerCase(Locale.ROOT); + return switch (language) { + case "en" -> switch (normalized) { + case "custom", "print_service" -> "Custom part request"; + case "series" -> "Series production request"; + case "consult", "design_service" -> "Consultation request"; + case "question" -> "General question"; + default -> requestType; + }; + case "de" -> switch (normalized) { + case "custom", "print_service" -> "Anfrage fuer Einzelteil"; + case "series" -> "Anfrage fuer Serienproduktion"; + case "consult", "design_service" -> "Beratungsanfrage"; + case "question" -> "Allgemeine Frage"; + default -> requestType; + }; + case "fr" -> switch (normalized) { + case "custom", "print_service" -> "Demande de piece personnalisee"; + case "series" -> "Demande de production en serie"; + case "consult", "design_service" -> "Demande de conseil"; + case "question" -> "Question generale"; + default -> requestType; + }; + default -> switch (normalized) { + case "custom", "print_service" -> "Richiesta pezzo personalizzato"; + case "series" -> "Richiesta produzione in serie"; + case "consult", "design_service" -> "Richiesta consulenza"; + case "question" -> "Domanda generale"; + default -> requestType; + }; + }; + } + + private String localizeCustomerType(String customerType, String language) { + if (customerType == null || customerType.isBlank()) { + return "-"; + } + String normalized = customerType.trim().toLowerCase(Locale.ROOT); + return switch (language) { + case "en" -> switch (normalized) { + case "private" -> "Private"; + case "business" -> "Business"; + default -> customerType; + }; + case "de" -> switch (normalized) { + case "private" -> "Privat"; + case "business" -> "Unternehmen"; + default -> customerType; + }; + case "fr" -> switch (normalized) { + case "private" -> "Prive"; + case "business" -> "Entreprise"; + default -> customerType; + }; + default -> switch (normalized) { + case "private" -> "Privato"; + case "business" -> "Azienda"; + default -> customerType; + }; + }; + } + + private Locale localeForLanguage(String language) { + return switch (language) { + case "en" -> Locale.ENGLISH; + case "de" -> Locale.GERMAN; + case "fr" -> Locale.FRENCH; + default -> Locale.ITALIAN; + }; + } + + private String normalizeLanguage(String language) { + if (language == null || language.isBlank()) { + return "it"; + } + String normalized = language.toLowerCase(Locale.ROOT).trim(); + if (normalized.startsWith("en")) { + return "en"; + } + if (normalized.startsWith("de")) { + return "de"; + } + if (normalized.startsWith("fr")) { + return "fr"; + } + return "it"; + } + + private String resolveRecipientName(CustomQuoteRequest request, String language) { + if (request.getName() != null && !request.getName().isBlank()) { + return request.getName().trim(); + } + if (request.getContactPerson() != null && !request.getContactPerson().isBlank()) { + return request.getContactPerson().trim(); + } + if (request.getCompanyName() != null && !request.getCompanyName().isBlank()) { + return request.getCompanyName().trim(); + } + return switch (language) { + case "en" -> "customer"; + case "de" -> "Kunde"; + case "fr" -> "client"; + default -> "cliente"; + }; + } + private String safeValue(String value) { if (value == null || value.isBlank()) { return "-"; diff --git a/backend/src/main/java/com/printcalculator/dto/QuoteRequestDto.java b/backend/src/main/java/com/printcalculator/dto/QuoteRequestDto.java index 70d36ba..afa8f1c 100644 --- a/backend/src/main/java/com/printcalculator/dto/QuoteRequestDto.java +++ b/backend/src/main/java/com/printcalculator/dto/QuoteRequestDto.java @@ -7,6 +7,7 @@ import jakarta.validation.constraints.AssertTrue; public class QuoteRequestDto { private String requestType; // "PRINT_SERVICE" or "DESIGN_SERVICE" private String customerType; // "PRIVATE" or "BUSINESS" + private String language; // "it" | "en" | "de" | "fr" private String email; private String phone; private String name; diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index ad6e2a0..3769350 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -44,6 +44,7 @@ app.mail.admin.enabled=${APP_MAIL_ADMIN_ENABLED:true} app.mail.admin.address=${APP_MAIL_ADMIN_ADDRESS:admin@printcalculator.local} app.mail.contact-request.admin.enabled=${APP_MAIL_CONTACT_REQUEST_ADMIN_ENABLED:true} app.mail.contact-request.admin.address=${APP_MAIL_CONTACT_REQUEST_ADMIN_ADDRESS:infog@3d-fab.ch} +app.mail.contact-request.customer.enabled=${APP_MAIL_CONTACT_REQUEST_CUSTOMER_ENABLED:true} app.frontend.base-url=${APP_FRONTEND_BASE_URL:http://localhost:4200} # Admin back-office authentication diff --git a/backend/src/main/resources/templates/email/contact-request-customer.html b/backend/src/main/resources/templates/email/contact-request-customer.html new file mode 100644 index 0000000..d308b0c --- /dev/null +++ b/backend/src/main/resources/templates/email/contact-request-customer.html @@ -0,0 +1,134 @@ + + + + + Contact request received + + + +
+

We received your contact request

+

Hi customer,

+

Thank you for contacting us. Our team will reply as soon as possible.

+

+ Please keep this request ID for future order references: + 00000000-0000-0000-0000-000000000000 +

+

Request details

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Request ID00000000-0000-0000-0000-000000000000
Date2026-03-03T10:00:00Z
Request typecustom
Customer typePRIVATE
NameMario Rossi
Company3D Fab SA
Contact personMario Rossi
Emailcliente@example.com
Phone+41 00 000 00 00
MessageTesto richiesta cliente...
Attachments0
+

If you need help, reply to this email.

+ + +
+ + diff --git a/frontend/src/app/core/services/quote-request.service.ts b/frontend/src/app/core/services/quote-request.service.ts index bb08dde..d44e180 100644 --- a/frontend/src/app/core/services/quote-request.service.ts +++ b/frontend/src/app/core/services/quote-request.service.ts @@ -6,6 +6,7 @@ import { environment } from '../../../environments/environment'; export interface QuoteRequestDto { requestType: string; customerType: string; + language?: 'it' | 'en' | 'de' | 'fr'; email: string; phone?: string; name?: string; diff --git a/frontend/src/app/features/about/about-page.component.ts b/frontend/src/app/features/about/about-page.component.ts index 2b093c8..ddb7caa 100644 --- a/frontend/src/app/features/about/about-page.component.ts +++ b/frontend/src/app/features/about/about-page.component.ts @@ -16,6 +16,7 @@ type PassionId = | 'woodworking' | 'van-life' | 'coffee' + | 'cooking' | 'software-development'; interface PassionChip { @@ -50,6 +51,7 @@ export class AboutPageComponent { { id: 'snowboard', labelKey: 'ABOUT.PASSION_SNOWBOARD' }, { id: 'van-life', labelKey: 'ABOUT.PASSION_VAN_LIFE' }, { id: 'self-hosting', labelKey: 'ABOUT.PASSION_SELF_HOSTING' }, + { id: 'cooking', labelKey: 'ABOUT.PASSION_COOKING' }, { id: 'snowboard-instructor', labelKey: 'ABOUT.PASSION_SNOWBOARD_INSTRUCTOR', @@ -67,6 +69,7 @@ export class AboutPageComponent { 'print-3d', 'travel', 'coffee', + 'cooking', 'software-development', ], matteo: [ diff --git a/frontend/src/app/features/admin/pages/admin-cad-invoices.component.html b/frontend/src/app/features/admin/pages/admin-cad-invoices.component.html index d8aed21..e1cdbb5 100644 --- a/frontend/src/app/features/admin/pages/admin-cad-invoices.component.html +++ b/frontend/src/app/features/admin/pages/admin-cad-invoices.component.html @@ -82,8 +82,15 @@ - {{ row.sessionId | slice: 0 : 8 }} - {{ row.sourceRequestId || "-" }} + + {{ row.sessionId | slice: 0 : 8 }} + + + {{ row.sourceRequestId || "-" }} + {{ row.cadHours }} {{ row.cadHourlyRateChf | currency: "CHF" }} {{ row.cadTotalChf | currency: "CHF" }} @@ -91,7 +98,11 @@ {{ row.sessionStatus }} {{ row.notes || "-" }} - + {{ row.convertedOrderId | slice: 0 : 8 }} ({{ row.convertedOrderStatus || "-" }}) diff --git a/frontend/src/app/features/admin/pages/admin-cad-invoices.component.ts b/frontend/src/app/features/admin/pages/admin-cad-invoices.component.ts index 0de7f4e..07bccf0 100644 --- a/frontend/src/app/features/admin/pages/admin-cad-invoices.component.ts +++ b/frontend/src/app/features/admin/pages/admin-cad-invoices.component.ts @@ -6,11 +6,12 @@ import { AdminOperationsService, } from '../services/admin-operations.service'; import { AdminOrdersService } from '../services/admin-orders.service'; +import { CopyOnClickDirective } from '../../../shared/directives/copy-on-click.directive'; @Component({ selector: 'app-admin-cad-invoices', standalone: true, - imports: [CommonModule, FormsModule], + imports: [CommonModule, FormsModule, CopyOnClickDirective], templateUrl: './admin-cad-invoices.component.html', styleUrl: './admin-cad-invoices.component.scss', }) diff --git a/frontend/src/app/features/admin/pages/admin-contact-requests.component.html b/frontend/src/app/features/admin/pages/admin-contact-requests.component.html index cd524eb..964adda 100644 --- a/frontend/src/app/features/admin/pages/admin-contact-requests.component.html +++ b/frontend/src/app/features/admin/pages/admin-contact-requests.component.html @@ -76,7 +76,10 @@

Dettaglio richiesta

- ID{{ selectedRequest.id }} + ID + {{ + selectedRequest.id + }}

diff --git a/frontend/src/app/features/admin/pages/admin-contact-requests.component.ts b/frontend/src/app/features/admin/pages/admin-contact-requests.component.ts index c99e810..13a2dc5 100644 --- a/frontend/src/app/features/admin/pages/admin-contact-requests.component.ts +++ b/frontend/src/app/features/admin/pages/admin-contact-requests.component.ts @@ -7,11 +7,12 @@ import { AdminContactRequestDetail, AdminOperationsService, } from '../services/admin-operations.service'; +import { CopyOnClickDirective } from '../../../shared/directives/copy-on-click.directive'; @Component({ selector: 'app-admin-contact-requests', standalone: true, - imports: [CommonModule, FormsModule], + imports: [CommonModule, FormsModule, CopyOnClickDirective], templateUrl: './admin-contact-requests.component.html', styleUrl: './admin-contact-requests.component.scss', }) diff --git a/frontend/src/app/features/admin/pages/admin-dashboard.component.html b/frontend/src/app/features/admin/pages/admin-dashboard.component.html index ce669d2..43c65c7 100644 --- a/frontend/src/app/features/admin/pages/admin-dashboard.component.html +++ b/frontend/src/app/features/admin/pages/admin-dashboard.component.html @@ -97,7 +97,10 @@

Dettaglio ordine {{ selectedOrder.orderNumber }}

- UUID: {{ selectedOrder.id }} + UUID: + {{ + selectedOrder.id + }}

Caricamento dettaglio...

diff --git a/frontend/src/app/features/admin/pages/admin-dashboard.component.ts b/frontend/src/app/features/admin/pages/admin-dashboard.component.ts index 336517d..8a3c23e 100644 --- a/frontend/src/app/features/admin/pages/admin-dashboard.component.ts +++ b/frontend/src/app/features/admin/pages/admin-dashboard.component.ts @@ -5,11 +5,12 @@ import { AdminOrder, AdminOrdersService, } from '../services/admin-orders.service'; +import { CopyOnClickDirective } from '../../../shared/directives/copy-on-click.directive'; @Component({ selector: 'app-admin-dashboard', standalone: true, - imports: [CommonModule, FormsModule], + imports: [CommonModule, FormsModule, CopyOnClickDirective], templateUrl: './admin-dashboard.component.html', styleUrl: './admin-dashboard.component.scss', }) diff --git a/frontend/src/app/features/admin/pages/admin-sessions.component.html b/frontend/src/app/features/admin/pages/admin-sessions.component.html index 6d1f28a..48492ef 100644 --- a/frontend/src/app/features/admin/pages/admin-sessions.component.html +++ b/frontend/src/app/features/admin/pages/admin-sessions.component.html @@ -33,12 +33,23 @@ - {{ session.id | slice: 0 : 8 }} + + {{ session.id | slice: 0 : 8 }} + {{ session.createdAt | date: "short" }} {{ session.expiresAt | date: "short" }} {{ session.materialCode }} {{ session.status }} - {{ session.convertedOrderId || "-" }} + + {{ + session.convertedOrderId + ? (session.convertedOrderId | slice: 0 : 8) + : "-" + }} + -