feat(front-end): make responsive back-office

This commit is contained in:
2026-03-03 08:43:13 +01:00
parent b7c399e3cb
commit 25afb355b4
16 changed files with 441 additions and 105 deletions

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,