feat(back-end and front-end) email for request
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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 || "-"
|
||||
}})
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 è",
|
||||
|
||||
Reference in New Issue
Block a user