dev #8

Closed
JoeKung wants to merge 72 commits from dev into int
16 changed files with 441 additions and 105 deletions
Showing only changes of commit 25afb355b4 - Show all commits

View File

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

View File

@@ -5,6 +5,7 @@ import com.printcalculator.dto.AdminContactRequestAttachmentDto;
import com.printcalculator.dto.AdminContactRequestDetailDto;
import com.printcalculator.dto.AdminFilamentStockDto;
import com.printcalculator.dto.AdminQuoteSessionDto;
import com.printcalculator.dto.AdminUpdateContactRequestStatusRequest;
import com.printcalculator.entity.CustomQuoteRequest;
import com.printcalculator.entity.CustomQuoteRequestAttachment;
import com.printcalculator.entity.FilamentVariant;
@@ -28,7 +29,9 @@ import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
@@ -41,15 +44,18 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.OffsetDateTime;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
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.INTERNAL_SERVER_ERROR;
import static org.springframework.http.HttpStatus.NOT_FOUND;
@@ -60,6 +66,9 @@ import static org.springframework.http.HttpStatus.NOT_FOUND;
public class AdminOperationsController {
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 Set<String> CONTACT_REQUEST_ALLOWED_STATUSES = Set.of(
"NEW", "PENDING", "IN_PROGRESS", "DONE", "CLOSED"
);
private final FilamentVariantStockKgRepository filamentStockRepo;
private final FilamentVariantRepository filamentVariantRepo;
@@ -156,22 +165,40 @@ public class AdminOperationsController {
.map(this::toContactRequestAttachmentDto)
.toList();
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 ResponseEntity.ok(toContactRequestDetailDto(request, 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")
@@ -291,6 +318,27 @@ public class AdminOperationsController {
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) {
AdminQuoteSessionDto dto = new AdminQuoteSessionDto();
dto.setId(session.getId());

View File

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

View File

@@ -10,7 +10,7 @@ FRONTEND_PORT=18082
CLAMAV_HOST=192.168.1.147
CLAMAV_PORT=3310
CLAMAV_ENABLED=true
APP_FRONTEND_BASE_URL=http://localhost:18082
APP_FRONTEND_BASE_URL=https://dev.3d-fab.ch
ADMIN_PASSWORD=
ADMIN_SESSION_SECRET=
ADMIN_SESSION_TTL_MINUTES=480

View File

@@ -10,7 +10,7 @@ FRONTEND_PORT=18081
CLAMAV_HOST=192.168.1.147
CLAMAV_PORT=3310
CLAMAV_ENABLED=true
APP_FRONTEND_BASE_URL=http://localhost:18081
APP_FRONTEND_BASE_URL=https://int.3d-fab.ch
ADMIN_PASSWORD=
ADMIN_SESSION_SECRET=
ADMIN_SESSION_TTL_MINUTES=480

View File

@@ -4,8 +4,8 @@ ENV=prod
TAG=prod
# Ports
BACKEND_PORT=8000
FRONTEND_PORT=80
BACKEND_PORT=18000
FRONTEND_PORT=18080
CLAMAV_HOST=192.168.1.147
CLAMAV_PORT=3310

View File

@@ -9,6 +9,7 @@
</header>
<p class="error" *ngIf="errorMessage">{{ errorMessage }}</p>
<p class="success" *ngIf="successMessage">{{ successMessage }}</p>
<div class="workspace" *ngIf="!loading; else loadingTpl">
<section class="list-panel">
@@ -80,6 +81,24 @@
<div class="meta-item"><dt>Referente</dt><dd>{{ selectedRequest.contactPerson || '-' }}</dd></div>
</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">
<h4>Messaggio</h4>
<p>{{ selectedRequest.message || '-' }}</p>

View File

@@ -2,7 +2,7 @@
background: var(--color-bg-card);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-6);
padding: clamp(12px, 2vw, 24px);
box-shadow: var(--shadow-sm);
}
@@ -44,11 +44,16 @@
.workspace {
display: grid;
grid-template-columns: minmax(500px, 1.25fr) minmax(420px, 1fr);
grid-template-columns: 1fr;
gap: var(--space-4);
align-items: start;
}
.list-panel,
.detail-panel {
min-width: 0;
}
button {
border: 0;
border-radius: var(--radius-md);
@@ -87,6 +92,7 @@ button.ghost {
table {
width: 100%;
border-collapse: collapse;
min-width: 760px;
}
thead {
@@ -308,6 +314,43 @@ tbody tr.selected {
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 {
display: inline-flex;
align-items: center;
@@ -356,9 +399,9 @@ button:disabled {
cursor: default;
}
@media (max-width: 1060px) {
@media (min-width: 1460px) {
.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;
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;
}
}

View File

@@ -1,5 +1,6 @@
import { CommonModule } from '@angular/common';
import { Component, inject, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import {
AdminContactRequest,
AdminContactRequestAttachment,
@@ -10,19 +11,23 @@ import {
@Component({
selector: 'app-admin-contact-requests',
standalone: true,
imports: [CommonModule],
imports: [CommonModule, FormsModule],
templateUrl: './admin-contact-requests.component.html',
styleUrl: './admin-contact-requests.component.scss'
})
export class AdminContactRequestsComponent implements OnInit {
private readonly adminOperationsService = inject(AdminOperationsService);
readonly statusOptions = ['NEW', 'PENDING', 'IN_PROGRESS', 'DONE', 'CLOSED'];
requests: AdminContactRequest[] = [];
selectedRequest: AdminContactRequestDetail | null = null;
selectedRequestId: string | null = null;
loading = false;
detailLoading = false;
updatingStatus = false;
selectedStatus = '';
errorMessage: string | null = null;
successMessage: string | null = null;
ngOnInit(): void {
this.loadRequests();
@@ -31,6 +36,7 @@ export class AdminContactRequestsComponent implements OnInit {
loadRequests(): void {
this.loading = true;
this.errorMessage = null;
this.successMessage = null;
this.adminOperationsService.getContactRequests().subscribe({
next: (requests) => {
this.requests = requests;
@@ -54,9 +60,11 @@ export class AdminContactRequestsComponent implements OnInit {
openDetails(requestId: string): void {
this.selectedRequestId = requestId;
this.detailLoading = true;
this.errorMessage = null;
this.adminOperationsService.getContactRequestDetail(requestId).subscribe({
next: (detail) => {
this.selectedRequest = detail;
this.selectedStatus = detail.status || '';
this.detailLoading = false;
},
error: () => {
@@ -111,6 +119,37 @@ export class AdminContactRequestsComponent implements OnInit {
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 {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');

View File

@@ -2,7 +2,7 @@
background: var(--color-bg-card);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-5);
padding: clamp(12px, 2vw, 20px);
box-shadow: var(--shadow-sm);
}
@@ -31,11 +31,16 @@
.workspace {
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);
align-items: start;
}
.list-panel,
.detail-panel {
min-width: 0;
}
button {
border: 0;
border-radius: var(--radius-md);
@@ -107,6 +112,7 @@ button:disabled {
table {
width: 100%;
border-collapse: collapse;
min-width: 760px;
}
thead {
@@ -218,7 +224,7 @@ tbody tr.no-results:hover {
}
.status-editor select {
min-width: 220px;
min-width: 210px;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-2) var(--space-3);
@@ -383,13 +389,21 @@ h4 {
font-size: 0.88rem;
}
@media (max-width: 1060px) {
@media (max-width: 1280px) {
.workspace {
grid-template-columns: 1fr;
}
.detail-panel {
min-height: unset;
}
}
@media (max-width: 820px) {
.admin-dashboard {
padding: var(--space-4);
}
.list-toolbar {
grid-template-columns: 1fr;
}
@@ -406,4 +420,43 @@ h4 {
.item {
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);
}
}

View File

@@ -2,7 +2,7 @@
background: var(--color-bg-card);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-6);
padding: clamp(12px, 2vw, 24px);
box-shadow: var(--shadow-sm);
}
@@ -205,6 +205,7 @@ select:disabled {
.variant-head-actions {
display: inline-flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-2);
}
@@ -346,6 +347,7 @@ button:disabled {
.dialog-actions {
display: flex;
justify-content: flex-end;
flex-wrap: wrap;
gap: var(--space-2);
}
@@ -355,8 +357,56 @@ button:disabled {
}
}
@media (max-width: 760px) {
.form-grid {
@media (max-width: 900px) {
.section-header {
flex-direction: column;
align-items: stretch;
}
.panel-header {
flex-wrap: wrap;
}
.material-grid {
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);
}
}

View File

@@ -3,7 +3,7 @@
justify-content: center;
align-items: center;
min-height: 70vh;
padding: var(--space-8) 0;
padding: var(--space-8) var(--space-3);
}
.admin-login-card {
@@ -73,3 +73,18 @@ button:disabled {
margin: 0;
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;
}
}

View File

@@ -2,7 +2,7 @@
background: var(--color-bg-card);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-6);
padding: clamp(12px, 2vw, 24px);
box-shadow: var(--shadow-sm);
}
@@ -67,6 +67,7 @@ button {
table {
width: 100%;
border-collapse: collapse;
min-width: 920px;
}
th,
@@ -112,6 +113,7 @@ td {
.detail-table {
width: 100%;
border-collapse: collapse;
min-width: 620px;
}
.detail-table th,
@@ -124,3 +126,39 @@ td {
.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;
}
}

View File

@@ -6,12 +6,14 @@
<p>Amministrazione operativa</p>
</div>
<nav class="menu">
<a routerLink="orders" routerLinkActive="active">Ordini</a>
<a routerLink="filament-stock" routerLinkActive="active">Stock filamenti</a>
<a routerLink="contact-requests" routerLinkActive="active">Richieste contatto</a>
<a routerLink="sessions" routerLinkActive="active">Sessioni</a>
</nav>
<div class="menu-scroll">
<nav class="menu">
<a routerLink="orders" routerLinkActive="active">Ordini</a>
<a routerLink="filament-stock" routerLinkActive="active">Stock filamenti</a>
<a routerLink="contact-requests" routerLinkActive="active">Richieste contatto</a>
<a routerLink="sessions" routerLinkActive="active">Sessioni</a>
</nav>
</div>
<button type="button" class="logout" (click)="logout()">Logout</button>
</aside>

View File

@@ -1,7 +1,7 @@
.admin-container {
margin-top: var(--space-8);
max-width: min(1720px, 96vw);
padding: 0 var(--space-6);
padding: 0 clamp(12px, 2.2vw, 24px);
}
.admin-shell {
@@ -42,6 +42,10 @@
gap: var(--space-2);
}
.menu-scroll {
min-width: 0;
}
.menu a {
text-decoration: none;
color: var(--color-text-muted);
@@ -51,6 +55,7 @@
border: 1px solid var(--color-border);
background: var(--color-bg-card);
transition: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease;
white-space: nowrap;
}
.menu a:hover {
@@ -87,10 +92,20 @@
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 {
margin-top: var(--space-6);
padding: 0 var(--space-4);
padding: 0 var(--space-3);
}
.admin-shell {
@@ -104,17 +119,39 @@
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 {
flex-direction: row;
flex-wrap: wrap;
flex-wrap: nowrap;
min-width: max-content;
}
.logout {
margin-top: var(--space-2);
align-self: flex-start;
margin-top: var(--space-1);
width: fit-content;
}
.content {
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);
}
}

View File

@@ -104,6 +104,10 @@ export interface AdminContactRequestDetail {
attachments: AdminContactRequestAttachment[];
}
export interface AdminUpdateContactRequestStatusPayload {
status: string;
}
export interface AdminQuoteSession {
id: string;
status: string;
@@ -187,6 +191,17 @@ export class AdminOperationsService {
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> {
return this.http.get(`${this.baseUrl}/contact-requests/${requestId}/attachments/${attachmentId}/file`, {
withCredentials: true,