produzione 1 #9
@@ -1,60 +0,0 @@
|
|||||||
package com.printcalculator.controller;
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Profile;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
import org.thymeleaf.TemplateEngine;
|
|
||||||
import org.thymeleaf.context.Context;
|
|
||||||
|
|
||||||
import java.time.OffsetDateTime;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/dev/email")
|
|
||||||
@Profile("local")
|
|
||||||
public class DevEmailTestController {
|
|
||||||
|
|
||||||
private final TemplateEngine templateEngine;
|
|
||||||
|
|
||||||
public DevEmailTestController(TemplateEngine templateEngine) {
|
|
||||||
this.templateEngine = templateEngine;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/test-template")
|
|
||||||
public ResponseEntity<String> testTemplate() {
|
|
||||||
Context context = new Context();
|
|
||||||
Map<String, Object> templateData = new HashMap<>();
|
|
||||||
UUID orderId = UUID.randomUUID();
|
|
||||||
templateData.put("customerName", "Mario Rossi");
|
|
||||||
templateData.put("orderId", orderId);
|
|
||||||
templateData.put("orderNumber", orderId.toString().split("-")[0]);
|
|
||||||
templateData.put("orderDetailsUrl", "https://tuosito.it/it/co/" + orderId);
|
|
||||||
templateData.put("orderDate", OffsetDateTime.now().format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm")));
|
|
||||||
templateData.put("totalCost", "CHF 45.50");
|
|
||||||
templateData.put("currentYear", OffsetDateTime.now().getYear());
|
|
||||||
templateData.put("emailTitle", "Conferma ordine");
|
|
||||||
templateData.put("headlineText", "Grazie per il tuo ordine #" + templateData.get("orderNumber"));
|
|
||||||
templateData.put("greetingText", "Ciao Mario,");
|
|
||||||
templateData.put("introText", "Abbiamo ricevuto il tuo ordine e iniziato l'elaborazione.");
|
|
||||||
templateData.put("detailsTitleText", "Dettagli ordine");
|
|
||||||
templateData.put("labelOrderNumber", "Numero ordine");
|
|
||||||
templateData.put("labelDate", "Data");
|
|
||||||
templateData.put("labelTotal", "Totale");
|
|
||||||
templateData.put("orderDetailsCtaText", "Visualizza stato ordine");
|
|
||||||
templateData.put("attachmentHintText", "In allegato trovi la conferma ordine in PDF con QR bill.");
|
|
||||||
templateData.put("supportText", "Se hai domande, rispondi a questa email.");
|
|
||||||
templateData.put("footerText", "Messaggio automatico di 3D-Fab.");
|
|
||||||
|
|
||||||
context.setVariables(templateData);
|
|
||||||
String html = templateEngine.process("email/order-confirmation", context);
|
|
||||||
|
|
||||||
return ResponseEntity.ok()
|
|
||||||
.header("Content-Type", "text/html; charset=utf-8")
|
|
||||||
.body(html);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,7 @@ import com.printcalculator.dto.AdminContactRequestAttachmentDto;
|
|||||||
import com.printcalculator.dto.AdminContactRequestDetailDto;
|
import com.printcalculator.dto.AdminContactRequestDetailDto;
|
||||||
import com.printcalculator.dto.AdminFilamentStockDto;
|
import com.printcalculator.dto.AdminFilamentStockDto;
|
||||||
import com.printcalculator.dto.AdminQuoteSessionDto;
|
import com.printcalculator.dto.AdminQuoteSessionDto;
|
||||||
|
import com.printcalculator.dto.AdminUpdateContactRequestStatusRequest;
|
||||||
import com.printcalculator.entity.CustomQuoteRequest;
|
import com.printcalculator.entity.CustomQuoteRequest;
|
||||||
import com.printcalculator.entity.CustomQuoteRequestAttachment;
|
import com.printcalculator.entity.CustomQuoteRequestAttachment;
|
||||||
import com.printcalculator.entity.FilamentVariant;
|
import com.printcalculator.entity.FilamentVariant;
|
||||||
@@ -28,7 +29,9 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PatchMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
@@ -41,15 +44,18 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static org.springframework.http.HttpStatus.BAD_REQUEST;
|
||||||
import static org.springframework.http.HttpStatus.CONFLICT;
|
import static org.springframework.http.HttpStatus.CONFLICT;
|
||||||
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
|
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
import static org.springframework.http.HttpStatus.NOT_FOUND;
|
import static org.springframework.http.HttpStatus.NOT_FOUND;
|
||||||
@@ -60,6 +66,9 @@ import static org.springframework.http.HttpStatus.NOT_FOUND;
|
|||||||
public class AdminOperationsController {
|
public class AdminOperationsController {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(AdminOperationsController.class);
|
private static final Logger logger = LoggerFactory.getLogger(AdminOperationsController.class);
|
||||||
private static final Path CONTACT_ATTACHMENTS_ROOT = Paths.get("storage_requests").toAbsolutePath().normalize();
|
private static final Path CONTACT_ATTACHMENTS_ROOT = Paths.get("storage_requests").toAbsolutePath().normalize();
|
||||||
|
private static final Set<String> CONTACT_REQUEST_ALLOWED_STATUSES = Set.of(
|
||||||
|
"NEW", "PENDING", "IN_PROGRESS", "DONE", "CLOSED"
|
||||||
|
);
|
||||||
|
|
||||||
private final FilamentVariantStockKgRepository filamentStockRepo;
|
private final FilamentVariantStockKgRepository filamentStockRepo;
|
||||||
private final FilamentVariantRepository filamentVariantRepo;
|
private final FilamentVariantRepository filamentVariantRepo;
|
||||||
@@ -156,22 +165,40 @@ public class AdminOperationsController {
|
|||||||
.map(this::toContactRequestAttachmentDto)
|
.map(this::toContactRequestAttachmentDto)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
AdminContactRequestDetailDto dto = new AdminContactRequestDetailDto();
|
return ResponseEntity.ok(toContactRequestDetailDto(request, attachments));
|
||||||
dto.setId(request.getId());
|
}
|
||||||
dto.setRequestType(request.getRequestType());
|
|
||||||
dto.setCustomerType(request.getCustomerType());
|
|
||||||
dto.setEmail(request.getEmail());
|
|
||||||
dto.setPhone(request.getPhone());
|
|
||||||
dto.setName(request.getName());
|
|
||||||
dto.setCompanyName(request.getCompanyName());
|
|
||||||
dto.setContactPerson(request.getContactPerson());
|
|
||||||
dto.setMessage(request.getMessage());
|
|
||||||
dto.setStatus(request.getStatus());
|
|
||||||
dto.setCreatedAt(request.getCreatedAt());
|
|
||||||
dto.setUpdatedAt(request.getUpdatedAt());
|
|
||||||
dto.setAttachments(attachments);
|
|
||||||
|
|
||||||
return ResponseEntity.ok(dto);
|
@PatchMapping("/contact-requests/{requestId}/status")
|
||||||
|
@Transactional
|
||||||
|
public ResponseEntity<AdminContactRequestDetailDto> updateContactRequestStatus(
|
||||||
|
@PathVariable UUID requestId,
|
||||||
|
@RequestBody AdminUpdateContactRequestStatusRequest payload
|
||||||
|
) {
|
||||||
|
CustomQuoteRequest request = customQuoteRequestRepo.findById(requestId)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Contact request not found"));
|
||||||
|
|
||||||
|
String requestedStatus = payload != null && payload.getStatus() != null
|
||||||
|
? payload.getStatus().trim().toUpperCase(Locale.ROOT)
|
||||||
|
: "";
|
||||||
|
|
||||||
|
if (!CONTACT_REQUEST_ALLOWED_STATUSES.contains(requestedStatus)) {
|
||||||
|
throw new ResponseStatusException(
|
||||||
|
BAD_REQUEST,
|
||||||
|
"Invalid status. Allowed: " + String.join(", ", CONTACT_REQUEST_ALLOWED_STATUSES)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
request.setStatus(requestedStatus);
|
||||||
|
request.setUpdatedAt(OffsetDateTime.now());
|
||||||
|
CustomQuoteRequest saved = customQuoteRequestRepo.save(request);
|
||||||
|
|
||||||
|
List<AdminContactRequestAttachmentDto> attachments = customQuoteRequestAttachmentRepo
|
||||||
|
.findByRequest_IdOrderByCreatedAtAsc(requestId)
|
||||||
|
.stream()
|
||||||
|
.map(this::toContactRequestAttachmentDto)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return ResponseEntity.ok(toContactRequestDetailDto(saved, attachments));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/contact-requests/{requestId}/attachments/{attachmentId}/file")
|
@GetMapping("/contact-requests/{requestId}/attachments/{attachmentId}/file")
|
||||||
@@ -291,6 +318,27 @@ public class AdminOperationsController {
|
|||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private AdminContactRequestDetailDto toContactRequestDetailDto(
|
||||||
|
CustomQuoteRequest request,
|
||||||
|
List<AdminContactRequestAttachmentDto> attachments
|
||||||
|
) {
|
||||||
|
AdminContactRequestDetailDto dto = new AdminContactRequestDetailDto();
|
||||||
|
dto.setId(request.getId());
|
||||||
|
dto.setRequestType(request.getRequestType());
|
||||||
|
dto.setCustomerType(request.getCustomerType());
|
||||||
|
dto.setEmail(request.getEmail());
|
||||||
|
dto.setPhone(request.getPhone());
|
||||||
|
dto.setName(request.getName());
|
||||||
|
dto.setCompanyName(request.getCompanyName());
|
||||||
|
dto.setContactPerson(request.getContactPerson());
|
||||||
|
dto.setMessage(request.getMessage());
|
||||||
|
dto.setStatus(request.getStatus());
|
||||||
|
dto.setCreatedAt(request.getCreatedAt());
|
||||||
|
dto.setUpdatedAt(request.getUpdatedAt());
|
||||||
|
dto.setAttachments(attachments);
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
private AdminQuoteSessionDto toQuoteSessionDto(QuoteSession session) {
|
private AdminQuoteSessionDto toQuoteSessionDto(QuoteSession session) {
|
||||||
AdminQuoteSessionDto dto = new AdminQuoteSessionDto();
|
AdminQuoteSessionDto dto = new AdminQuoteSessionDto();
|
||||||
dto.setId(session.getId());
|
dto.setId(session.getId());
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.printcalculator.dto;
|
||||||
|
|
||||||
|
public class AdminUpdateContactRequestStatusRequest {
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ FRONTEND_PORT=18082
|
|||||||
CLAMAV_HOST=192.168.1.147
|
CLAMAV_HOST=192.168.1.147
|
||||||
CLAMAV_PORT=3310
|
CLAMAV_PORT=3310
|
||||||
CLAMAV_ENABLED=true
|
CLAMAV_ENABLED=true
|
||||||
APP_FRONTEND_BASE_URL=http://localhost:18082
|
APP_FRONTEND_BASE_URL=https://dev.3d-fab.ch
|
||||||
ADMIN_PASSWORD=
|
ADMIN_PASSWORD=
|
||||||
ADMIN_SESSION_SECRET=
|
ADMIN_SESSION_SECRET=
|
||||||
ADMIN_SESSION_TTL_MINUTES=480
|
ADMIN_SESSION_TTL_MINUTES=480
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ FRONTEND_PORT=18081
|
|||||||
CLAMAV_HOST=192.168.1.147
|
CLAMAV_HOST=192.168.1.147
|
||||||
CLAMAV_PORT=3310
|
CLAMAV_PORT=3310
|
||||||
CLAMAV_ENABLED=true
|
CLAMAV_ENABLED=true
|
||||||
APP_FRONTEND_BASE_URL=http://localhost:18081
|
APP_FRONTEND_BASE_URL=https://int.3d-fab.ch
|
||||||
ADMIN_PASSWORD=
|
ADMIN_PASSWORD=
|
||||||
ADMIN_SESSION_SECRET=
|
ADMIN_SESSION_SECRET=
|
||||||
ADMIN_SESSION_TTL_MINUTES=480
|
ADMIN_SESSION_TTL_MINUTES=480
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ ENV=prod
|
|||||||
TAG=prod
|
TAG=prod
|
||||||
|
|
||||||
# Ports
|
# Ports
|
||||||
BACKEND_PORT=8000
|
BACKEND_PORT=18000
|
||||||
FRONTEND_PORT=80
|
FRONTEND_PORT=18080
|
||||||
|
|
||||||
CLAMAV_HOST=192.168.1.147
|
CLAMAV_HOST=192.168.1.147
|
||||||
CLAMAV_PORT=3310
|
CLAMAV_PORT=3310
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<p class="error" *ngIf="errorMessage">{{ errorMessage }}</p>
|
<p class="error" *ngIf="errorMessage">{{ errorMessage }}</p>
|
||||||
|
<p class="success" *ngIf="successMessage">{{ successMessage }}</p>
|
||||||
|
|
||||||
<div class="workspace" *ngIf="!loading; else loadingTpl">
|
<div class="workspace" *ngIf="!loading; else loadingTpl">
|
||||||
<section class="list-panel">
|
<section class="list-panel">
|
||||||
@@ -80,6 +81,24 @@
|
|||||||
<div class="meta-item"><dt>Referente</dt><dd>{{ selectedRequest.contactPerson || '-' }}</dd></div>
|
<div class="meta-item"><dt>Referente</dt><dd>{{ selectedRequest.contactPerson || '-' }}</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
|
<div class="status-editor">
|
||||||
|
<div class="status-editor-field">
|
||||||
|
<label for="contact-request-status">Stato richiesta</label>
|
||||||
|
<select
|
||||||
|
id="contact-request-status"
|
||||||
|
[ngModel]="selectedStatus"
|
||||||
|
(ngModelChange)="selectedStatus = $event">
|
||||||
|
<option *ngFor="let status of statusOptions" [ngValue]="status">{{ status }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
(click)="updateRequestStatus()"
|
||||||
|
[disabled]="!selectedRequest || updatingStatus || !selectedStatus || selectedStatus === selectedRequest.status">
|
||||||
|
{{ updatingStatus ? 'Salvataggio...' : 'Aggiorna stato' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="message-box">
|
<div class="message-box">
|
||||||
<h4>Messaggio</h4>
|
<h4>Messaggio</h4>
|
||||||
<p>{{ selectedRequest.message || '-' }}</p>
|
<p>{{ selectedRequest.message || '-' }}</p>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
background: var(--color-bg-card);
|
background: var(--color-bg-card);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
padding: var(--space-6);
|
padding: clamp(12px, 2vw, 24px);
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,11 +44,16 @@
|
|||||||
|
|
||||||
.workspace {
|
.workspace {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(500px, 1.25fr) minmax(420px, 1fr);
|
grid-template-columns: 1fr;
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-panel,
|
||||||
|
.detail-panel {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
@@ -87,6 +92,7 @@ button.ghost {
|
|||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
min-width: 760px;
|
||||||
}
|
}
|
||||||
|
|
||||||
thead {
|
thead {
|
||||||
@@ -308,6 +314,43 @@ tbody tr.selected {
|
|||||||
margin-bottom: var(--space-3);
|
margin-bottom: var(--space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
color: #157347;
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-editor {
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--color-neutral-100);
|
||||||
|
padding: var(--space-3);
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-editor-field {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-1);
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-editor-field label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-editor-field select {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
background: var(--color-bg-card);
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
.chip {
|
.chip {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -356,9 +399,9 @@ button:disabled {
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1060px) {
|
@media (min-width: 1460px) {
|
||||||
.workspace {
|
.workspace {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: minmax(0, 1.2fr) minmax(340px, 0.9fr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,4 +428,28 @@ button:disabled {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
padding: var(--space-3);
|
padding: var(--space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.request-id code {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-editor {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-editor button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.section-card {
|
||||||
|
padding: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: var(--space-2);
|
||||||
|
font-size: 0.86rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Component, inject, OnInit } from '@angular/core';
|
import { Component, inject, OnInit } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
import {
|
import {
|
||||||
AdminContactRequest,
|
AdminContactRequest,
|
||||||
AdminContactRequestAttachment,
|
AdminContactRequestAttachment,
|
||||||
@@ -10,19 +11,23 @@ import {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-admin-contact-requests',
|
selector: 'app-admin-contact-requests',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule],
|
imports: [CommonModule, FormsModule],
|
||||||
templateUrl: './admin-contact-requests.component.html',
|
templateUrl: './admin-contact-requests.component.html',
|
||||||
styleUrl: './admin-contact-requests.component.scss'
|
styleUrl: './admin-contact-requests.component.scss'
|
||||||
})
|
})
|
||||||
export class AdminContactRequestsComponent implements OnInit {
|
export class AdminContactRequestsComponent implements OnInit {
|
||||||
private readonly adminOperationsService = inject(AdminOperationsService);
|
private readonly adminOperationsService = inject(AdminOperationsService);
|
||||||
|
|
||||||
|
readonly statusOptions = ['NEW', 'PENDING', 'IN_PROGRESS', 'DONE', 'CLOSED'];
|
||||||
requests: AdminContactRequest[] = [];
|
requests: AdminContactRequest[] = [];
|
||||||
selectedRequest: AdminContactRequestDetail | null = null;
|
selectedRequest: AdminContactRequestDetail | null = null;
|
||||||
selectedRequestId: string | null = null;
|
selectedRequestId: string | null = null;
|
||||||
loading = false;
|
loading = false;
|
||||||
detailLoading = false;
|
detailLoading = false;
|
||||||
|
updatingStatus = false;
|
||||||
|
selectedStatus = '';
|
||||||
errorMessage: string | null = null;
|
errorMessage: string | null = null;
|
||||||
|
successMessage: string | null = null;
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.loadRequests();
|
this.loadRequests();
|
||||||
@@ -31,6 +36,7 @@ export class AdminContactRequestsComponent implements OnInit {
|
|||||||
loadRequests(): void {
|
loadRequests(): void {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.errorMessage = null;
|
this.errorMessage = null;
|
||||||
|
this.successMessage = null;
|
||||||
this.adminOperationsService.getContactRequests().subscribe({
|
this.adminOperationsService.getContactRequests().subscribe({
|
||||||
next: (requests) => {
|
next: (requests) => {
|
||||||
this.requests = requests;
|
this.requests = requests;
|
||||||
@@ -54,9 +60,11 @@ export class AdminContactRequestsComponent implements OnInit {
|
|||||||
openDetails(requestId: string): void {
|
openDetails(requestId: string): void {
|
||||||
this.selectedRequestId = requestId;
|
this.selectedRequestId = requestId;
|
||||||
this.detailLoading = true;
|
this.detailLoading = true;
|
||||||
|
this.errorMessage = null;
|
||||||
this.adminOperationsService.getContactRequestDetail(requestId).subscribe({
|
this.adminOperationsService.getContactRequestDetail(requestId).subscribe({
|
||||||
next: (detail) => {
|
next: (detail) => {
|
||||||
this.selectedRequest = detail;
|
this.selectedRequest = detail;
|
||||||
|
this.selectedStatus = detail.status || '';
|
||||||
this.detailLoading = false;
|
this.detailLoading = false;
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
@@ -111,6 +119,37 @@ export class AdminContactRequestsComponent implements OnInit {
|
|||||||
return 'chip-light';
|
return 'chip-light';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateRequestStatus(): void {
|
||||||
|
if (!this.selectedRequest || !this.selectedRequestId || !this.selectedStatus || this.updatingStatus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.errorMessage = null;
|
||||||
|
this.successMessage = null;
|
||||||
|
this.updatingStatus = true;
|
||||||
|
|
||||||
|
this.adminOperationsService.updateContactRequestStatus(this.selectedRequestId, { status: this.selectedStatus }).subscribe({
|
||||||
|
next: (updated) => {
|
||||||
|
this.selectedRequest = updated;
|
||||||
|
this.selectedStatus = updated.status || this.selectedStatus;
|
||||||
|
this.requests = this.requests.map(request =>
|
||||||
|
request.id === updated.id
|
||||||
|
? {
|
||||||
|
...request,
|
||||||
|
status: updated.status
|
||||||
|
}
|
||||||
|
: request
|
||||||
|
);
|
||||||
|
this.updatingStatus = false;
|
||||||
|
this.successMessage = 'Stato richiesta aggiornato.';
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.updatingStatus = false;
|
||||||
|
this.errorMessage = 'Impossibile aggiornare lo stato della richiesta.';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private downloadBlob(blob: Blob, filename: string): void {
|
private downloadBlob(blob: Blob, filename: string): void {
|
||||||
const url = window.URL.createObjectURL(blob);
|
const url = window.URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
background: var(--color-bg-card);
|
background: var(--color-bg-card);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
padding: var(--space-5);
|
padding: clamp(12px, 2vw, 20px);
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,11 +31,16 @@
|
|||||||
|
|
||||||
.workspace {
|
.workspace {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(540px, 1.35fr) minmax(420px, 0.95fr);
|
grid-template-columns: minmax(0, 1.2fr) minmax(340px, 0.9fr);
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-panel,
|
||||||
|
.detail-panel {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
@@ -107,6 +112,7 @@ button:disabled {
|
|||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
min-width: 760px;
|
||||||
}
|
}
|
||||||
|
|
||||||
thead {
|
thead {
|
||||||
@@ -218,7 +224,7 @@ tbody tr.no-results:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.status-editor select {
|
.status-editor select {
|
||||||
min-width: 220px;
|
min-width: 210px;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
padding: var(--space-2) var(--space-3);
|
padding: var(--space-2) var(--space-3);
|
||||||
@@ -383,13 +389,21 @@ h4 {
|
|||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1060px) {
|
@media (max-width: 1280px) {
|
||||||
.workspace {
|
.workspace {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-panel {
|
||||||
|
min-height: unset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 820px) {
|
@media (max-width: 820px) {
|
||||||
|
.admin-dashboard {
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
.list-toolbar {
|
.list-toolbar {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
@@ -406,4 +420,43 @@ h4 {
|
|||||||
.item {
|
.item {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actions-block {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-editor {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-editor select {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-actions button {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.admin-dashboard {
|
||||||
|
padding: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: var(--space-2);
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-backdrop {
|
||||||
|
padding: var(--space-2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
background: var(--color-bg-card);
|
background: var(--color-bg-card);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
padding: var(--space-6);
|
padding: clamp(12px, 2vw, 24px);
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,6 +205,7 @@ select:disabled {
|
|||||||
.variant-head-actions {
|
.variant-head-actions {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,6 +347,7 @@ button:disabled {
|
|||||||
.dialog-actions {
|
.dialog-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,8 +357,56 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 900px) {
|
||||||
.form-grid {
|
.section-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.section-card {
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.variant-header {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.variant-head-main {
|
||||||
|
width: 100%;
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.variant-head-actions {
|
||||||
|
width: 100%;
|
||||||
|
order: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-toggle {
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel button,
|
||||||
|
.subpanel button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.section-card {
|
||||||
|
padding: var(--space-3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 70vh;
|
min-height: 70vh;
|
||||||
padding: var(--space-8) 0;
|
padding: var(--space-8) var(--space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-login-card {
|
.admin-login-card {
|
||||||
@@ -73,3 +73,18 @@ button:disabled {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.admin-login-page {
|
||||||
|
min-height: 64vh;
|
||||||
|
padding: var(--space-5) var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-login-card {
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
background: var(--color-bg-card);
|
background: var(--color-bg-card);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
padding: var(--space-6);
|
padding: clamp(12px, 2vw, 24px);
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +67,7 @@ button {
|
|||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
min-width: 920px;
|
||||||
}
|
}
|
||||||
|
|
||||||
th,
|
th,
|
||||||
@@ -112,6 +113,7 @@ td {
|
|||||||
.detail-table {
|
.detail-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
min-width: 620px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-table th,
|
.detail-table th,
|
||||||
@@ -124,3 +126,39 @@ td {
|
|||||||
.muted {
|
.muted {
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.section-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-cell {
|
||||||
|
padding: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-box {
|
||||||
|
padding: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-summary {
|
||||||
|
gap: var(--space-2);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.section-card {
|
||||||
|
padding: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: var(--space-2);
|
||||||
|
font-size: 0.86rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,12 +6,14 @@
|
|||||||
<p>Amministrazione operativa</p>
|
<p>Amministrazione operativa</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="menu">
|
<div class="menu-scroll">
|
||||||
<a routerLink="orders" routerLinkActive="active">Ordini</a>
|
<nav class="menu">
|
||||||
<a routerLink="filament-stock" routerLinkActive="active">Stock filamenti</a>
|
<a routerLink="orders" routerLinkActive="active">Ordini</a>
|
||||||
<a routerLink="contact-requests" routerLinkActive="active">Richieste contatto</a>
|
<a routerLink="filament-stock" routerLinkActive="active">Stock filamenti</a>
|
||||||
<a routerLink="sessions" routerLinkActive="active">Sessioni</a>
|
<a routerLink="contact-requests" routerLinkActive="active">Richieste contatto</a>
|
||||||
</nav>
|
<a routerLink="sessions" routerLinkActive="active">Sessioni</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="button" class="logout" (click)="logout()">Logout</button>
|
<button type="button" class="logout" (click)="logout()">Logout</button>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.admin-container {
|
.admin-container {
|
||||||
margin-top: var(--space-8);
|
margin-top: var(--space-8);
|
||||||
max-width: min(1720px, 96vw);
|
max-width: min(1720px, 96vw);
|
||||||
padding: 0 var(--space-6);
|
padding: 0 clamp(12px, 2.2vw, 24px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-shell {
|
.admin-shell {
|
||||||
@@ -42,6 +42,10 @@
|
|||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-scroll {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.menu a {
|
.menu a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
@@ -51,6 +55,7 @@
|
|||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
background: var(--color-bg-card);
|
background: var(--color-bg-card);
|
||||||
transition: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease;
|
transition: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu a:hover {
|
.menu a:hover {
|
||||||
@@ -87,10 +92,20 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 960px) {
|
@media (max-width: 1240px) {
|
||||||
|
.admin-shell {
|
||||||
|
grid-template-columns: 220px minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
padding: var(--space-5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1360px) {
|
||||||
.admin-container {
|
.admin-container {
|
||||||
margin-top: var(--space-6);
|
margin-top: var(--space-6);
|
||||||
padding: 0 var(--space-4);
|
padding: 0 var(--space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-shell {
|
.admin-shell {
|
||||||
@@ -104,17 +119,39 @@
|
|||||||
padding: var(--space-4);
|
padding: var(--space-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-scroll {
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
margin: 0 calc(-1 * var(--space-1));
|
||||||
|
padding-inline: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
.menu {
|
.menu {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
|
min-width: max-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logout {
|
.logout {
|
||||||
margin-top: var(--space-2);
|
margin-top: var(--space-1);
|
||||||
align-self: flex-start;
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
padding: var(--space-4);
|
padding: var(--space-4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.brand h1 {
|
||||||
|
font-size: 1.02rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand p {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: var(--space-3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -104,6 +104,10 @@ export interface AdminContactRequestDetail {
|
|||||||
attachments: AdminContactRequestAttachment[];
|
attachments: AdminContactRequestAttachment[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminUpdateContactRequestStatusPayload {
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AdminQuoteSession {
|
export interface AdminQuoteSession {
|
||||||
id: string;
|
id: string;
|
||||||
status: string;
|
status: string;
|
||||||
@@ -187,6 +191,17 @@ export class AdminOperationsService {
|
|||||||
return this.http.get<AdminContactRequestDetail>(`${this.baseUrl}/contact-requests/${requestId}`, { withCredentials: true });
|
return this.http.get<AdminContactRequestDetail>(`${this.baseUrl}/contact-requests/${requestId}`, { withCredentials: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateContactRequestStatus(
|
||||||
|
requestId: string,
|
||||||
|
payload: AdminUpdateContactRequestStatusPayload
|
||||||
|
): Observable<AdminContactRequestDetail> {
|
||||||
|
return this.http.patch<AdminContactRequestDetail>(
|
||||||
|
`${this.baseUrl}/contact-requests/${requestId}/status`,
|
||||||
|
payload,
|
||||||
|
{ withCredentials: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
downloadContactRequestAttachment(requestId: string, attachmentId: string): Observable<Blob> {
|
downloadContactRequestAttachment(requestId: string, attachmentId: string): Observable<Blob> {
|
||||||
return this.http.get(`${this.baseUrl}/contact-requests/${requestId}/attachments/${attachmentId}/file`, {
|
return this.http.get(`${this.baseUrl}/contact-requests/${requestId}/attachments/${attachmentId}/file`, {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user