feat(front-end): make responsive back-office
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user