feat(back-end and front-end) email for request
All checks were successful
PR Checks / prettier-autofix (pull_request) Successful in 18s
PR Checks / security-sast (pull_request) Successful in 33s
PR Checks / test-backend (pull_request) Successful in 27s
PR Checks / test-frontend (pull_request) Successful in 1m8s

This commit is contained in:
2026-03-04 12:15:23 +01:00
parent 1b3f0b16ff
commit 767b65008b
20 changed files with 493 additions and 55 deletions

View File

@@ -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;

View File

@@ -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: [

View File

@@ -82,8 +82,15 @@
</thead>
<tbody>
<tr *ngFor="let row of invoices">
<td [title]="row.sessionId">{{ row.sessionId | slice: 0 : 8 }}</td>
<td>{{ row.sourceRequestId || "-" }}</td>
<td [title]="row.sessionId" [appCopyOnClick]="row.sessionId">
{{ row.sessionId | slice: 0 : 8 }}
</td>
<td
[title]="row.sourceRequestId || ''"
[appCopyOnClick]="row.sourceRequestId"
>
{{ row.sourceRequestId || "-" }}
</td>
<td>{{ row.cadHours }}</td>
<td>{{ row.cadHourlyRateChf | currency: "CHF" }}</td>
<td>{{ row.cadTotalChf | currency: "CHF" }}</td>
@@ -91,7 +98,11 @@
<td>{{ row.sessionStatus }}</td>
<td class="notes-cell" [title]="row.notes || ''">{{ row.notes || "-" }}</td>
<td>
<span *ngIf="row.convertedOrderId; else noOrder">
<span
*ngIf="row.convertedOrderId; else noOrder"
[title]="row.convertedOrderId || ''"
[appCopyOnClick]="row.convertedOrderId"
>
{{ row.convertedOrderId | slice: 0 : 8 }} ({{
row.convertedOrderStatus || "-"
}})

View File

@@ -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',
})

View File

@@ -76,7 +76,10 @@
<div>
<h3>Dettaglio richiesta</h3>
<p class="request-id">
<span>ID</span><code>{{ selectedRequest.id }}</code>
<span>ID</span>
<code [title]="selectedRequest.id" [appCopyOnClick]="selectedRequest.id">{{
selectedRequest.id
}}</code>
</p>
</div>
<div class="detail-chips">

View File

@@ -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',
})

View File

@@ -97,7 +97,10 @@
<div class="detail-header">
<h2>Dettaglio ordine {{ selectedOrder.orderNumber }}</h2>
<p class="order-uuid">
UUID: <code>{{ selectedOrder.id }}</code>
UUID:
<code [title]="selectedOrder.id" [appCopyOnClick]="selectedOrder.id">{{
selectedOrder.id
}}</code>
</p>
<p *ngIf="detailLoading">Caricamento dettaglio...</p>
</div>

View File

@@ -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',
})

View File

@@ -33,12 +33,23 @@
<tbody>
<ng-container *ngFor="let session of sessions">
<tr>
<td [title]="session.id">{{ session.id | slice: 0 : 8 }}</td>
<td [title]="session.id" [appCopyOnClick]="session.id">
{{ session.id | slice: 0 : 8 }}
</td>
<td>{{ session.createdAt | date: "short" }}</td>
<td>{{ session.expiresAt | date: "short" }}</td>
<td>{{ session.materialCode }}</td>
<td>{{ session.status }}</td>
<td>{{ session.convertedOrderId || "-" }}</td>
<td
[title]="session.convertedOrderId || ''"
[appCopyOnClick]="session.convertedOrderId"
>
{{
session.convertedOrderId
? (session.convertedOrderId | slice: 0 : 8)
: "-"
}}
</td>
<td class="actions">
<button
type="button"
@@ -47,13 +58,6 @@
>
{{ isDetailOpen(session.id) ? "Nascondi" : "Vedi" }}
</button>
<button
type="button"
class="btn-secondary"
(click)="copySessionUuid(session.id)"
>
Copia UUID
</button>
<button
type="button"
class="btn-danger"
@@ -87,7 +91,11 @@
>
<div class="detail-session-id">
<strong>UUID sessione:</strong>
<code>{{ detail.session.id }}</code>
<code
[title]="detail.session.id"
[appCopyOnClick]="detail.session.id"
>{{ detail.session.id }}</code
>
</div>
<div class="detail-summary">

View File

@@ -5,11 +5,12 @@ import {
AdminQuoteSession,
AdminQuoteSessionDetail,
} from '../services/admin-operations.service';
import { CopyOnClickDirective } from '../../../shared/directives/copy-on-click.directive';
@Component({
selector: 'app-admin-sessions',
standalone: true,
imports: [CommonModule],
imports: [CommonModule, CopyOnClickDirective],
templateUrl: './admin-sessions.component.html',
styleUrl: './admin-sessions.component.scss',
})
@@ -135,42 +136,6 @@ export class AdminSessionsComponent implements OnInit {
return `${hours}h ${minutes}m`;
}
copySessionUuid(sessionId: string): void {
if (!sessionId) {
return;
}
if (navigator.clipboard?.writeText) {
navigator.clipboard.writeText(sessionId).then(
() => {
this.errorMessage = null;
this.successMessage = 'UUID sessione copiato.';
},
() => {
this.errorMessage = 'Impossibile copiare UUID sessione.';
},
);
return;
}
// Fallback for older browsers.
const textarea = document.createElement('textarea');
textarea.value = sessionId;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
this.errorMessage = null;
this.successMessage = 'UUID sessione copiato.';
} catch {
this.errorMessage = 'Impossibile copiare UUID sessione.';
} finally {
document.body.removeChild(textarea);
}
}
private extractErrorMessage(error: unknown, fallback: string): string {
const err = error as { error?: { message?: string } };
return err?.error?.message || fallback;

View File

@@ -11,6 +11,7 @@ import { AppInputComponent } from '../../../../shared/components/app-input/app-i
import { AppButtonComponent } from '../../../../shared/components/app-button/app-button.component';
import { QuoteEstimatorService } from '../../../calculator/services/quote-estimator.service';
import { QuoteRequestService } from '../../../../core/services/quote-request.service';
import { LanguageService } from '../../../../core/services/language.service';
interface FilePreview {
file: File;
@@ -53,6 +54,7 @@ export class ContactFormComponent implements OnDestroy {
];
private quoteRequestService = inject(QuoteRequestService);
private languageService = inject(LanguageService);
constructor(
private fb: FormBuilder,
@@ -257,6 +259,7 @@ export class ContactFormComponent implements OnDestroy {
const requestDto: any = {
requestType: formVal.requestType,
customerType: isCompany ? 'BUSINESS' : 'PRIVATE',
language: this.languageService.selectedLang(),
email: formVal.email,
phone: formVal.phone,
message: formVal.message,

View File

@@ -0,0 +1,45 @@
import { Directive, HostBinding, HostListener, Input } from '@angular/core';
@Directive({
selector: '[appCopyOnClick]',
standalone: true,
})
export class CopyOnClickDirective {
@Input('appCopyOnClick') value: string | null | undefined;
@HostBinding('style.cursor') readonly cursor = 'pointer';
@HostListener('click', ['$event'])
onClick(event: MouseEvent): void {
const text = (this.value ?? '').trim();
if (!text) {
return;
}
event.stopPropagation();
void this.copy(text);
}
private async copy(text: string): Promise<void> {
if (navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(text);
return;
} catch {
// Fallback below for browsers/environments that block clipboard API.
}
}
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
} finally {
document.body.removeChild(textarea);
}
}
}

View File

@@ -149,6 +149,7 @@
"PASSION_WOODWORKING": "Holzbearbeitung",
"PASSION_VAN_LIFE": "Van Life",
"PASSION_COFFEE": "Kaffee",
"PASSION_COOKING": "Kochen",
"PASSION_SOFTWARE_DEVELOPMENT": "Softwareentwicklung",
"SERVICES_TITLE": "Hauptleistungen",
"TARGET_TITLE": "Für wen",

View File

@@ -149,6 +149,7 @@
"PASSION_WOODWORKING": "Woodworking",
"PASSION_VAN_LIFE": "Van life",
"PASSION_COFFEE": "Coffee",
"PASSION_COOKING": "Cooking",
"PASSION_SOFTWARE_DEVELOPMENT": "Software development",
"SERVICES_TITLE": "Main Services",
"TARGET_TITLE": "Who is it for",

View File

@@ -206,6 +206,7 @@
"PASSION_WOODWORKING": "Travail du bois",
"PASSION_VAN_LIFE": "Van life",
"PASSION_COFFEE": "Café",
"PASSION_COOKING": "Cuisine",
"PASSION_SOFTWARE_DEVELOPMENT": "Développement logiciel",
"SERVICES_TITLE": "Services principaux",
"TARGET_TITLE": "Pour qui",

View File

@@ -206,6 +206,7 @@
"PASSION_WOODWORKING": "Lavorazione del legno",
"PASSION_VAN_LIFE": "Van life",
"PASSION_COFFEE": "Caffè",
"PASSION_COOKING": "Cucina",
"PASSION_SOFTWARE_DEVELOPMENT": "Sviluppo software",
"SERVICES_TITLE": "Servizi principali",
"TARGET_TITLE": "Per chi è",