From 25afb355b4379d3c42bc8b0c97e77ab68a575505 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Tue, 3 Mar 2026 08:43:13 +0100 Subject: [PATCH] feat(front-end): make responsive back-office --- .../controller/DevEmailTestController.java | 60 -------------- .../admin/AdminOperationsController.java | 78 +++++++++++++++---- ...dminUpdateContactRequestStatusRequest.java | 13 ++++ deploy/envs/dev.env | 2 +- deploy/envs/int.env | 2 +- deploy/envs/prod.env | 4 +- .../admin-contact-requests.component.html | 19 +++++ .../admin-contact-requests.component.scss | 75 +++++++++++++++++- .../pages/admin-contact-requests.component.ts | 41 +++++++++- .../pages/admin-dashboard.component.scss | 61 ++++++++++++++- .../pages/admin-filament-stock.component.scss | 56 ++++++++++++- .../admin/pages/admin-login.component.scss | 17 +++- .../admin/pages/admin-sessions.component.scss | 40 +++++++++- .../admin/pages/admin-shell.component.html | 14 ++-- .../admin/pages/admin-shell.component.scss | 49 ++++++++++-- .../services/admin-operations.service.ts | 15 ++++ 16 files changed, 441 insertions(+), 105 deletions(-) delete mode 100644 backend/src/main/java/com/printcalculator/controller/DevEmailTestController.java create mode 100644 backend/src/main/java/com/printcalculator/dto/AdminUpdateContactRequestStatusRequest.java diff --git a/backend/src/main/java/com/printcalculator/controller/DevEmailTestController.java b/backend/src/main/java/com/printcalculator/controller/DevEmailTestController.java deleted file mode 100644 index ab2be20..0000000 --- a/backend/src/main/java/com/printcalculator/controller/DevEmailTestController.java +++ /dev/null @@ -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 testTemplate() { - Context context = new Context(); - Map 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); - } -} diff --git a/backend/src/main/java/com/printcalculator/controller/admin/AdminOperationsController.java b/backend/src/main/java/com/printcalculator/controller/admin/AdminOperationsController.java index 29219de..f026884 100644 --- a/backend/src/main/java/com/printcalculator/controller/admin/AdminOperationsController.java +++ b/backend/src/main/java/com/printcalculator/controller/admin/AdminOperationsController.java @@ -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 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 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 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 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()); diff --git a/backend/src/main/java/com/printcalculator/dto/AdminUpdateContactRequestStatusRequest.java b/backend/src/main/java/com/printcalculator/dto/AdminUpdateContactRequestStatusRequest.java new file mode 100644 index 0000000..c12abb0 --- /dev/null +++ b/backend/src/main/java/com/printcalculator/dto/AdminUpdateContactRequestStatusRequest.java @@ -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; + } +} diff --git a/deploy/envs/dev.env b/deploy/envs/dev.env index 8a82535..8de2ed7 100644 --- a/deploy/envs/dev.env +++ b/deploy/envs/dev.env @@ -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 diff --git a/deploy/envs/int.env b/deploy/envs/int.env index c04a1bf..9f1118e 100644 --- a/deploy/envs/int.env +++ b/deploy/envs/int.env @@ -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 diff --git a/deploy/envs/prod.env b/deploy/envs/prod.env index 87fe496..a85cf00 100644 --- a/deploy/envs/prod.env +++ b/deploy/envs/prod.env @@ -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 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 713f20b..8822f83 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 @@ -9,6 +9,7 @@

{{ errorMessage }}

+

{{ successMessage }}

@@ -80,6 +81,24 @@
Referente
{{ selectedRequest.contactPerson || '-' }}
+
+
+ + +
+ +
+

Messaggio

{{ selectedRequest.message || '-' }}

diff --git a/frontend/src/app/features/admin/pages/admin-contact-requests.component.scss b/frontend/src/app/features/admin/pages/admin-contact-requests.component.scss index 5724cf6..25e5e2d 100644 --- a/frontend/src/app/features/admin/pages/admin-contact-requests.component.scss +++ b/frontend/src/app/features/admin/pages/admin-contact-requests.component.scss @@ -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; + } } 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 9213583..dd55cbd 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 @@ -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'); diff --git a/frontend/src/app/features/admin/pages/admin-dashboard.component.scss b/frontend/src/app/features/admin/pages/admin-dashboard.component.scss index 68373dd..0e406f2 100644 --- a/frontend/src/app/features/admin/pages/admin-dashboard.component.scss +++ b/frontend/src/app/features/admin/pages/admin-dashboard.component.scss @@ -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); + } } diff --git a/frontend/src/app/features/admin/pages/admin-filament-stock.component.scss b/frontend/src/app/features/admin/pages/admin-filament-stock.component.scss index 822bea9..49bdf6c 100644 --- a/frontend/src/app/features/admin/pages/admin-filament-stock.component.scss +++ b/frontend/src/app/features/admin/pages/admin-filament-stock.component.scss @@ -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); + } +} diff --git a/frontend/src/app/features/admin/pages/admin-login.component.scss b/frontend/src/app/features/admin/pages/admin-login.component.scss index 8be1bfd..c5bc32c 100644 --- a/frontend/src/app/features/admin/pages/admin-login.component.scss +++ b/frontend/src/app/features/admin/pages/admin-login.component.scss @@ -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; + } +} diff --git a/frontend/src/app/features/admin/pages/admin-sessions.component.scss b/frontend/src/app/features/admin/pages/admin-sessions.component.scss index 4dee770..10b0ed1 100644 --- a/frontend/src/app/features/admin/pages/admin-sessions.component.scss +++ b/frontend/src/app/features/admin/pages/admin-sessions.component.scss @@ -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; + } +} diff --git a/frontend/src/app/features/admin/pages/admin-shell.component.html b/frontend/src/app/features/admin/pages/admin-shell.component.html index 583d1d8..8e39e15 100644 --- a/frontend/src/app/features/admin/pages/admin-shell.component.html +++ b/frontend/src/app/features/admin/pages/admin-shell.component.html @@ -6,12 +6,14 @@

Amministrazione operativa

- + diff --git a/frontend/src/app/features/admin/pages/admin-shell.component.scss b/frontend/src/app/features/admin/pages/admin-shell.component.scss index 9e28d2a..5cf0645 100644 --- a/frontend/src/app/features/admin/pages/admin-shell.component.scss +++ b/frontend/src/app/features/admin/pages/admin-shell.component.scss @@ -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); + } +} diff --git a/frontend/src/app/features/admin/services/admin-operations.service.ts b/frontend/src/app/features/admin/services/admin-operations.service.ts index a693732..de517c9 100644 --- a/frontend/src/app/features/admin/services/admin-operations.service.ts +++ b/frontend/src/app/features/admin/services/admin-operations.service.ts @@ -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(`${this.baseUrl}/contact-requests/${requestId}`, { withCredentials: true }); } + updateContactRequestStatus( + requestId: string, + payload: AdminUpdateContactRequestStatusPayload + ): Observable { + return this.http.patch( + `${this.baseUrl}/contact-requests/${requestId}/status`, + payload, + { withCredentials: true } + ); + } + downloadContactRequestAttachment(requestId: string, attachmentId: string): Observable { return this.http.get(`${this.baseUrl}/contact-requests/${requestId}/attachments/${attachmentId}/file`, { withCredentials: true,