style: apply prettier formatting
This commit is contained in:
@@ -5,7 +5,9 @@
|
||||
<p>Richieste preventivo personalizzato ricevute dal sito.</p>
|
||||
<span class="total-pill">{{ requests.length }} richieste</span>
|
||||
</div>
|
||||
<button type="button" (click)="loadRequests()" [disabled]="loading">Aggiorna</button>
|
||||
<button type="button" (click)="loadRequests()" [disabled]="loading">
|
||||
Aggiorna
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<p class="error" *ngIf="errorMessage">{{ errorMessage }}</p>
|
||||
@@ -32,10 +34,19 @@
|
||||
[class.selected]="isSelected(request.id)"
|
||||
(click)="openDetails(request.id)"
|
||||
>
|
||||
<td class="created-at">{{ request.createdAt | date:'short' }}</td>
|
||||
<td class="created-at">
|
||||
{{ request.createdAt | date: "short" }}
|
||||
</td>
|
||||
<td class="name-cell">
|
||||
<p class="primary">{{ request.name || request.companyName || '-' }}</p>
|
||||
<p class="secondary" *ngIf="request.name && request.companyName">{{ request.companyName }}</p>
|
||||
<p class="primary">
|
||||
{{ request.name || request.companyName || "-" }}
|
||||
</p>
|
||||
<p
|
||||
class="secondary"
|
||||
*ngIf="request.name && request.companyName"
|
||||
>
|
||||
{{ request.companyName }}
|
||||
</p>
|
||||
</td>
|
||||
<td class="email-cell">{{ request.email }}</td>
|
||||
<td>
|
||||
@@ -45,7 +56,11 @@
|
||||
<span class="chip chip-light">{{ request.customerType }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="chip" [ngClass]="getStatusChipClass(request.status)">{{ request.status }}</span>
|
||||
<span
|
||||
class="chip"
|
||||
[ngClass]="getStatusChipClass(request.status)"
|
||||
>{{ request.status }}</span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="empty-row" *ngIf="requests.length === 0">
|
||||
@@ -60,25 +75,58 @@
|
||||
<header class="detail-header">
|
||||
<div>
|
||||
<h3>Dettaglio richiesta</h3>
|
||||
<p class="request-id"><span>ID</span><code>{{ selectedRequest.id }}</code></p>
|
||||
<p class="request-id">
|
||||
<span>ID</span><code>{{ selectedRequest.id }}</code>
|
||||
</p>
|
||||
</div>
|
||||
<div class="detail-chips">
|
||||
<span class="chip" [ngClass]="getStatusChipClass(selectedRequest.status)">{{ selectedRequest.status }}</span>
|
||||
<span class="chip chip-neutral">{{ selectedRequest.requestType }}</span>
|
||||
<span class="chip chip-light">{{ selectedRequest.customerType }}</span>
|
||||
<span
|
||||
class="chip"
|
||||
[ngClass]="getStatusChipClass(selectedRequest.status)"
|
||||
>{{ selectedRequest.status }}</span
|
||||
>
|
||||
<span class="chip chip-neutral">{{
|
||||
selectedRequest.requestType
|
||||
}}</span>
|
||||
<span class="chip chip-light">{{
|
||||
selectedRequest.customerType
|
||||
}}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<p class="loading-detail" *ngIf="detailLoading">Caricamento dettaglio...</p>
|
||||
<p class="loading-detail" *ngIf="detailLoading">
|
||||
Caricamento dettaglio...
|
||||
</p>
|
||||
|
||||
<dl class="meta-grid">
|
||||
<div class="meta-item"><dt>Creata</dt><dd>{{ selectedRequest.createdAt | date:'medium' }}</dd></div>
|
||||
<div class="meta-item"><dt>Aggiornata</dt><dd>{{ selectedRequest.updatedAt | date:'medium' }}</dd></div>
|
||||
<div class="meta-item"><dt>Email</dt><dd>{{ selectedRequest.email }}</dd></div>
|
||||
<div class="meta-item"><dt>Telefono</dt><dd>{{ selectedRequest.phone || '-' }}</dd></div>
|
||||
<div class="meta-item"><dt>Nome</dt><dd>{{ selectedRequest.name || '-' }}</dd></div>
|
||||
<div class="meta-item"><dt>Azienda</dt><dd>{{ selectedRequest.companyName || '-' }}</dd></div>
|
||||
<div class="meta-item"><dt>Referente</dt><dd>{{ selectedRequest.contactPerson || '-' }}</dd></div>
|
||||
<div class="meta-item">
|
||||
<dt>Creata</dt>
|
||||
<dd>{{ selectedRequest.createdAt | date: "medium" }}</dd>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<dt>Aggiornata</dt>
|
||||
<dd>{{ selectedRequest.updatedAt | date: "medium" }}</dd>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<dt>Email</dt>
|
||||
<dd>{{ selectedRequest.email }}</dd>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<dt>Telefono</dt>
|
||||
<dd>{{ selectedRequest.phone || "-" }}</dd>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<dt>Nome</dt>
|
||||
<dd>{{ selectedRequest.name || "-" }}</dd>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<dt>Azienda</dt>
|
||||
<dd>{{ selectedRequest.companyName || "-" }}</dd>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<dt>Referente</dt>
|
||||
<dd>{{ selectedRequest.contactPerson || "-" }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div class="status-editor">
|
||||
@@ -87,36 +135,61 @@
|
||||
<select
|
||||
id="contact-request-status"
|
||||
[ngModel]="selectedStatus"
|
||||
(ngModelChange)="selectedStatus = $event">
|
||||
<option *ngFor="let status of statusOptions" [ngValue]="status">{{ status }}</option>
|
||||
(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' }}
|
||||
[disabled]="
|
||||
!selectedRequest ||
|
||||
updatingStatus ||
|
||||
!selectedStatus ||
|
||||
selectedStatus === selectedRequest.status
|
||||
"
|
||||
>
|
||||
{{ updatingStatus ? "Salvataggio..." : "Aggiorna stato" }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="message-box">
|
||||
<h4>Messaggio</h4>
|
||||
<p>{{ selectedRequest.message || '-' }}</p>
|
||||
<p>{{ selectedRequest.message || "-" }}</p>
|
||||
</div>
|
||||
|
||||
<div class="attachments">
|
||||
<h4>Allegati</h4>
|
||||
<div class="attachment-list" *ngIf="selectedRequest.attachments.length > 0; else noAttachmentsTpl">
|
||||
<article class="attachment-item" *ngFor="let attachment of selectedRequest.attachments">
|
||||
<div
|
||||
class="attachment-list"
|
||||
*ngIf="selectedRequest.attachments.length > 0; else noAttachmentsTpl"
|
||||
>
|
||||
<article
|
||||
class="attachment-item"
|
||||
*ngFor="let attachment of selectedRequest.attachments"
|
||||
>
|
||||
<div>
|
||||
<p class="filename">{{ attachment.originalFilename }}</p>
|
||||
<p class="meta">
|
||||
{{ formatFileSize(attachment.fileSizeBytes) }}
|
||||
<span *ngIf="attachment.mimeType"> | {{ attachment.mimeType }}</span>
|
||||
<span *ngIf="attachment.createdAt"> | {{ attachment.createdAt | date:'short' }}</span>
|
||||
<span *ngIf="attachment.mimeType">
|
||||
| {{ attachment.mimeType }}</span
|
||||
>
|
||||
<span *ngIf="attachment.createdAt">
|
||||
| {{ attachment.createdAt | date: "short" }}</span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="ghost" (click)="downloadAttachment(attachment)">Scarica file</button>
|
||||
<button
|
||||
type="button"
|
||||
class="ghost"
|
||||
(click)="downloadAttachment(attachment)"
|
||||
>
|
||||
Scarica file
|
||||
</button>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -62,7 +62,9 @@ button {
|
||||
padding: var(--space-2) var(--space-4);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease, opacity 0.2s ease;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
opacity 0.2s ease;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
AdminContactRequest,
|
||||
AdminContactRequestAttachment,
|
||||
AdminContactRequestDetail,
|
||||
AdminOperationsService
|
||||
AdminOperationsService,
|
||||
} from '../services/admin-operations.service';
|
||||
|
||||
@Component({
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule],
|
||||
templateUrl: './admin-contact-requests.component.html',
|
||||
styleUrl: './admin-contact-requests.component.scss'
|
||||
styleUrl: './admin-contact-requests.component.scss',
|
||||
})
|
||||
export class AdminContactRequestsComponent implements OnInit {
|
||||
private readonly adminOperationsService = inject(AdminOperationsService);
|
||||
@@ -43,7 +43,10 @@ export class AdminContactRequestsComponent implements OnInit {
|
||||
if (requests.length === 0) {
|
||||
this.selectedRequest = null;
|
||||
this.selectedRequestId = null;
|
||||
} else if (this.selectedRequestId && requests.some(r => r.id === this.selectedRequestId)) {
|
||||
} else if (
|
||||
this.selectedRequestId &&
|
||||
requests.some((r) => r.id === this.selectedRequestId)
|
||||
) {
|
||||
this.openDetails(this.selectedRequestId);
|
||||
} else {
|
||||
this.openDetails(requests[0].id);
|
||||
@@ -53,7 +56,7 @@ export class AdminContactRequestsComponent implements OnInit {
|
||||
error: () => {
|
||||
this.loading = false;
|
||||
this.errorMessage = 'Impossibile caricare le richieste di contatto.';
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -70,7 +73,7 @@ export class AdminContactRequestsComponent implements OnInit {
|
||||
error: () => {
|
||||
this.detailLoading = false;
|
||||
this.errorMessage = 'Impossibile caricare il dettaglio richiesta.';
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -83,12 +86,18 @@ export class AdminContactRequestsComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
this.adminOperationsService.downloadContactRequestAttachment(this.selectedRequest.id, attachment.id).subscribe({
|
||||
next: (blob) => this.downloadBlob(blob, attachment.originalFilename || `attachment-${attachment.id}`),
|
||||
error: () => {
|
||||
this.errorMessage = 'Download allegato non riuscito.';
|
||||
}
|
||||
});
|
||||
this.adminOperationsService
|
||||
.downloadContactRequestAttachment(this.selectedRequest.id, attachment.id)
|
||||
.subscribe({
|
||||
next: (blob) =>
|
||||
this.downloadBlob(
|
||||
blob,
|
||||
attachment.originalFilename || `attachment-${attachment.id}`,
|
||||
),
|
||||
error: () => {
|
||||
this.errorMessage = 'Download allegato non riuscito.';
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
formatFileSize(bytes?: number): string {
|
||||
@@ -120,7 +129,12 @@ export class AdminContactRequestsComponent implements OnInit {
|
||||
}
|
||||
|
||||
updateRequestStatus(): void {
|
||||
if (!this.selectedRequest || !this.selectedRequestId || !this.selectedStatus || this.updatingStatus) {
|
||||
if (
|
||||
!this.selectedRequest ||
|
||||
!this.selectedRequestId ||
|
||||
!this.selectedStatus ||
|
||||
this.updatingStatus
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -128,26 +142,31 @@ export class AdminContactRequestsComponent implements OnInit {
|
||||
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.';
|
||||
}
|
||||
});
|
||||
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 {
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
<p>Seleziona un ordine a sinistra e gestiscilo nel dettaglio a destra.</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button type="button" (click)="loadOrders()" [disabled]="loading">Aggiorna</button>
|
||||
<button type="button" (click)="loadOrders()" [disabled]="loading">
|
||||
Aggiorna
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -32,7 +34,12 @@
|
||||
[ngModel]="paymentStatusFilter"
|
||||
(ngModelChange)="onPaymentStatusFilterChange($event)"
|
||||
>
|
||||
<option *ngFor="let option of paymentStatusFilterOptions" [ngValue]="option">{{ option }}</option>
|
||||
<option
|
||||
*ngFor="let option of paymentStatusFilterOptions"
|
||||
[ngValue]="option"
|
||||
>
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="toolbar-field" for="order-status-filter">
|
||||
@@ -42,7 +49,12 @@
|
||||
[ngModel]="orderStatusFilter"
|
||||
(ngModelChange)="onOrderStatusFilterChange($event)"
|
||||
>
|
||||
<option *ngFor="let option of orderStatusFilterOptions" [ngValue]="option">{{ option }}</option>
|
||||
<option
|
||||
*ngFor="let option of orderStatusFilterOptions"
|
||||
[ngValue]="option"
|
||||
>
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
@@ -65,12 +77,16 @@
|
||||
>
|
||||
<td>{{ order.orderNumber }}</td>
|
||||
<td>{{ order.customerEmail }}</td>
|
||||
<td>{{ order.paymentStatus || 'PENDING' }}</td>
|
||||
<td>{{ order.paymentStatus || "PENDING" }}</td>
|
||||
<td>{{ order.status }}</td>
|
||||
<td>{{ order.totalChf | currency:'CHF':'symbol':'1.2-2' }}</td>
|
||||
<td>
|
||||
{{ order.totalChf | currency: "CHF" : "symbol" : "1.2-2" }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="no-results" *ngIf="filteredOrders.length === 0">
|
||||
<td colspan="5">Nessun ordine trovato per i filtri selezionati.</td>
|
||||
<td colspan="5">
|
||||
Nessun ordine trovato per i filtri selezionati.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -80,39 +96,74 @@
|
||||
<section class="detail-panel" *ngIf="selectedOrder">
|
||||
<div class="detail-header">
|
||||
<h2>Dettaglio ordine {{ selectedOrder.orderNumber }}</h2>
|
||||
<p class="order-uuid">UUID: <code>{{ selectedOrder.id }}</code></p>
|
||||
<p class="order-uuid">
|
||||
UUID: <code>{{ selectedOrder.id }}</code>
|
||||
</p>
|
||||
<p *ngIf="detailLoading">Caricamento dettaglio...</p>
|
||||
</div>
|
||||
|
||||
<div class="meta-grid">
|
||||
<div><strong>Cliente</strong><span>{{ selectedOrder.customerEmail }}</span></div>
|
||||
<div><strong>Stato pagamento</strong><span>{{ selectedOrder.paymentStatus || 'PENDING' }}</span></div>
|
||||
<div><strong>Stato ordine</strong><span>{{ selectedOrder.status }}</span></div>
|
||||
<div><strong>Totale</strong><span>{{ selectedOrder.totalChf | currency:'CHF':'symbol':'1.2-2' }}</span></div>
|
||||
<div>
|
||||
<strong>Cliente</strong><span>{{ selectedOrder.customerEmail }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Stato pagamento</strong
|
||||
><span>{{ selectedOrder.paymentStatus || "PENDING" }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Stato ordine</strong><span>{{ selectedOrder.status }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Totale</strong
|
||||
><span>{{
|
||||
selectedOrder.totalChf | currency: "CHF" : "symbol" : "1.2-2"
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions-block">
|
||||
<div class="status-editor">
|
||||
<label for="order-status">Stato ordine</label>
|
||||
<select id="order-status" [value]="selectedStatus" (change)="onStatusChange($event)">
|
||||
<option *ngFor="let option of orderStatusOptions" [value]="option">{{ option }}</option>
|
||||
<select
|
||||
id="order-status"
|
||||
[value]="selectedStatus"
|
||||
(change)="onStatusChange($event)"
|
||||
>
|
||||
<option *ngFor="let option of orderStatusOptions" [value]="option">
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
<button type="button" (click)="updateStatus()" [disabled]="updatingStatus">
|
||||
{{ updatingStatus ? 'Salvataggio...' : 'Aggiorna stato' }}
|
||||
<button
|
||||
type="button"
|
||||
(click)="updateStatus()"
|
||||
[disabled]="updatingStatus"
|
||||
>
|
||||
{{ updatingStatus ? "Salvataggio..." : "Aggiorna stato" }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="status-editor">
|
||||
<label for="payment-method">Metodo pagamento</label>
|
||||
<select id="payment-method" [value]="selectedPaymentMethod" (change)="onPaymentMethodChange($event)">
|
||||
<option *ngFor="let option of paymentMethodOptions" [value]="option">{{ option }}</option>
|
||||
<select
|
||||
id="payment-method"
|
||||
[value]="selectedPaymentMethod"
|
||||
(change)="onPaymentMethodChange($event)"
|
||||
>
|
||||
<option
|
||||
*ngFor="let option of paymentMethodOptions"
|
||||
[value]="option"
|
||||
>
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
(click)="confirmPayment()"
|
||||
[disabled]="confirmingPayment || selectedOrder.paymentStatus === 'COMPLETED'"
|
||||
[disabled]="
|
||||
confirmingPayment || selectedOrder.paymentStatus === 'COMPLETED'
|
||||
"
|
||||
>
|
||||
{{ confirmingPayment ? 'Invio...' : 'Conferma pagamento' }}
|
||||
{{ confirmingPayment ? "Invio..." : "Conferma pagamento" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -132,17 +183,26 @@
|
||||
<div class="items">
|
||||
<div class="item" *ngFor="let item of selectedOrder.items">
|
||||
<div class="item-main">
|
||||
<p class="file-name"><strong>{{ item.originalFilename }}</strong></p>
|
||||
<p class="file-name">
|
||||
<strong>{{ item.originalFilename }}</strong>
|
||||
</p>
|
||||
<p class="item-meta">
|
||||
Qta: {{ item.quantity }} |
|
||||
Colore:
|
||||
<span class="color-swatch" *ngIf="isHexColor(item.colorCode)" [style.background-color]="item.colorCode"></span>
|
||||
<span>{{ item.colorCode || '-' }}</span>
|
||||
|
|
||||
Riga: {{ item.lineTotalChf | currency:'CHF':'symbol':'1.2-2' }}
|
||||
Qta: {{ item.quantity }} | Colore:
|
||||
<span
|
||||
class="color-swatch"
|
||||
*ngIf="isHexColor(item.colorCode)"
|
||||
[style.background-color]="item.colorCode"
|
||||
></span>
|
||||
<span>{{ item.colorCode || "-" }}</span>
|
||||
| Riga:
|
||||
{{ item.lineTotalChf | currency: "CHF" : "symbol" : "1.2-2" }}
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="ghost" (click)="downloadItemFile(item.id, item.originalFilename)">
|
||||
<button
|
||||
type="button"
|
||||
class="ghost"
|
||||
(click)="downloadItemFile(item.id, item.originalFilename)"
|
||||
>
|
||||
Scarica file
|
||||
</button>
|
||||
</div>
|
||||
@@ -160,21 +220,52 @@
|
||||
<p>Caricamento ordini...</p>
|
||||
</ng-template>
|
||||
|
||||
<div class="modal-backdrop" *ngIf="showPrintDetails && selectedOrder" (click)="closePrintDetails()">
|
||||
<div
|
||||
class="modal-backdrop"
|
||||
*ngIf="showPrintDetails && selectedOrder"
|
||||
(click)="closePrintDetails()"
|
||||
>
|
||||
<div class="modal-card" (click)="$event.stopPropagation()">
|
||||
<header class="modal-header">
|
||||
<h3>Dettagli stampa ordine {{ selectedOrder.orderNumber }}</h3>
|
||||
<button type="button" class="ghost close-btn" (click)="closePrintDetails()">Chiudi</button>
|
||||
<button
|
||||
type="button"
|
||||
class="ghost close-btn"
|
||||
(click)="closePrintDetails()"
|
||||
>
|
||||
Chiudi
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="modal-grid">
|
||||
<div><strong>Qualità</strong><span>{{ getQualityLabel(selectedOrder.printLayerHeightMm) }}</span></div>
|
||||
<div><strong>Materiale</strong><span>{{ selectedOrder.printMaterialCode || '-' }}</span></div>
|
||||
<div><strong>Layer height</strong><span>{{ selectedOrder.printLayerHeightMm || '-' }} mm</span></div>
|
||||
<div><strong>Nozzle</strong><span>{{ selectedOrder.printNozzleDiameterMm || '-' }} mm</span></div>
|
||||
<div><strong>Infill pattern</strong><span>{{ selectedOrder.printInfillPattern || '-' }}</span></div>
|
||||
<div><strong>Infill %</strong><span>{{ selectedOrder.printInfillPercent ?? '-' }}</span></div>
|
||||
<div><strong>Supporti</strong><span>{{ selectedOrder.printSupportsEnabled ? 'Sì' : 'No' }}</span></div>
|
||||
<div>
|
||||
<strong>Qualità</strong
|
||||
><span>{{ getQualityLabel(selectedOrder.printLayerHeightMm) }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Materiale</strong
|
||||
><span>{{ selectedOrder.printMaterialCode || "-" }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Layer height</strong
|
||||
><span>{{ selectedOrder.printLayerHeightMm || "-" }} mm</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Nozzle</strong
|
||||
><span>{{ selectedOrder.printNozzleDiameterMm || "-" }} mm</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Infill pattern</strong
|
||||
><span>{{ selectedOrder.printInfillPattern || "-" }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Infill %</strong
|
||||
><span>{{ selectedOrder.printInfillPercent ?? "-" }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Supporti</strong
|
||||
><span>{{ selectedOrder.printSupportsEnabled ? "Sì" : "No" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>Colori file</h4>
|
||||
@@ -182,8 +273,12 @@
|
||||
<div class="file-color-row" *ngFor="let item of selectedOrder.items">
|
||||
<span class="filename">{{ item.originalFilename }}</span>
|
||||
<span class="file-color">
|
||||
<span class="color-swatch" *ngIf="isHexColor(item.colorCode)" [style.background-color]="item.colorCode"></span>
|
||||
{{ item.colorCode || '-' }}
|
||||
<span
|
||||
class="color-swatch"
|
||||
*ngIf="isHexColor(item.colorCode)"
|
||||
[style.background-color]="item.colorCode"
|
||||
></span>
|
||||
{{ item.colorCode || "-" }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -75,7 +75,10 @@ button:disabled {
|
||||
|
||||
.list-toolbar {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(230px, 1.6fr) minmax(170px, 1fr) minmax(190px, 1fr);
|
||||
grid-template-columns: minmax(230px, 1.6fr) minmax(170px, 1fr) minmax(
|
||||
190px,
|
||||
1fr
|
||||
);
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { AdminOrder, AdminOrdersService } from '../services/admin-orders.service';
|
||||
import {
|
||||
AdminOrder,
|
||||
AdminOrdersService,
|
||||
} from '../services/admin-orders.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin-dashboard',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule],
|
||||
templateUrl: './admin-dashboard.component.html',
|
||||
styleUrl: './admin-dashboard.component.scss'
|
||||
styleUrl: './admin-dashboard.component.scss',
|
||||
})
|
||||
export class AdminDashboardComponent implements OnInit {
|
||||
private readonly adminOrdersService = inject(AdminOrdersService);
|
||||
@@ -33,10 +36,21 @@ export class AdminDashboardComponent implements OnInit {
|
||||
'IN_PRODUCTION',
|
||||
'SHIPPED',
|
||||
'COMPLETED',
|
||||
'CANCELLED'
|
||||
'CANCELLED',
|
||||
];
|
||||
readonly paymentMethodOptions = [
|
||||
'TWINT',
|
||||
'BANK_TRANSFER',
|
||||
'CARD',
|
||||
'CASH',
|
||||
'OTHER',
|
||||
];
|
||||
readonly paymentStatusFilterOptions = [
|
||||
'ALL',
|
||||
'PENDING',
|
||||
'REPORTED',
|
||||
'COMPLETED',
|
||||
];
|
||||
readonly paymentMethodOptions = ['TWINT', 'BANK_TRANSFER', 'CARD', 'CASH', 'OTHER'];
|
||||
readonly paymentStatusFilterOptions = ['ALL', 'PENDING', 'REPORTED', 'COMPLETED'];
|
||||
readonly orderStatusFilterOptions = [
|
||||
'ALL',
|
||||
'PENDING_PAYMENT',
|
||||
@@ -44,7 +58,7 @@ export class AdminDashboardComponent implements OnInit {
|
||||
'IN_PRODUCTION',
|
||||
'SHIPPED',
|
||||
'COMPLETED',
|
||||
'CANCELLED'
|
||||
'CANCELLED',
|
||||
];
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -62,8 +76,12 @@ export class AdminDashboardComponent implements OnInit {
|
||||
if (!this.selectedOrder && this.filteredOrders.length > 0) {
|
||||
this.openDetails(this.filteredOrders[0].id);
|
||||
} else if (this.selectedOrder) {
|
||||
const exists = orders.find(order => order.id === this.selectedOrder?.id);
|
||||
const selectedIsVisible = this.filteredOrders.some(order => order.id === this.selectedOrder?.id);
|
||||
const exists = orders.find(
|
||||
(order) => order.id === this.selectedOrder?.id,
|
||||
);
|
||||
const selectedIsVisible = this.filteredOrders.some(
|
||||
(order) => order.id === this.selectedOrder?.id,
|
||||
);
|
||||
if (exists && selectedIsVisible) {
|
||||
this.openDetails(exists.id);
|
||||
} else if (this.filteredOrders.length > 0) {
|
||||
@@ -78,7 +96,7 @@ export class AdminDashboardComponent implements OnInit {
|
||||
error: () => {
|
||||
this.loading = false;
|
||||
this.errorMessage = 'Impossibile caricare gli ordini.';
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -109,7 +127,7 @@ export class AdminDashboardComponent implements OnInit {
|
||||
error: () => {
|
||||
this.detailLoading = false;
|
||||
this.errorMessage = 'Impossibile caricare il dettaglio ordine.';
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -119,36 +137,44 @@ export class AdminDashboardComponent implements OnInit {
|
||||
}
|
||||
|
||||
this.confirmingPayment = true;
|
||||
this.adminOrdersService.confirmPayment(this.selectedOrder.id, this.selectedPaymentMethod).subscribe({
|
||||
next: (updatedOrder) => {
|
||||
this.confirmingPayment = false;
|
||||
this.applyOrderUpdate(updatedOrder);
|
||||
},
|
||||
error: () => {
|
||||
this.confirmingPayment = false;
|
||||
this.errorMessage = 'Conferma pagamento non riuscita.';
|
||||
}
|
||||
});
|
||||
this.adminOrdersService
|
||||
.confirmPayment(this.selectedOrder.id, this.selectedPaymentMethod)
|
||||
.subscribe({
|
||||
next: (updatedOrder) => {
|
||||
this.confirmingPayment = false;
|
||||
this.applyOrderUpdate(updatedOrder);
|
||||
},
|
||||
error: () => {
|
||||
this.confirmingPayment = false;
|
||||
this.errorMessage = 'Conferma pagamento non riuscita.';
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
updateStatus(): void {
|
||||
if (!this.selectedOrder || this.updatingStatus || !this.selectedStatus.trim()) {
|
||||
if (
|
||||
!this.selectedOrder ||
|
||||
this.updatingStatus ||
|
||||
!this.selectedStatus.trim()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updatingStatus = true;
|
||||
this.adminOrdersService.updateOrderStatus(this.selectedOrder.id, {
|
||||
status: this.selectedStatus.trim()
|
||||
}).subscribe({
|
||||
next: (updatedOrder) => {
|
||||
this.updatingStatus = false;
|
||||
this.applyOrderUpdate(updatedOrder);
|
||||
},
|
||||
error: () => {
|
||||
this.updatingStatus = false;
|
||||
this.errorMessage = 'Aggiornamento stato ordine non riuscito.';
|
||||
}
|
||||
});
|
||||
this.adminOrdersService
|
||||
.updateOrderStatus(this.selectedOrder.id, {
|
||||
status: this.selectedStatus.trim(),
|
||||
})
|
||||
.subscribe({
|
||||
next: (updatedOrder) => {
|
||||
this.updatingStatus = false;
|
||||
this.applyOrderUpdate(updatedOrder);
|
||||
},
|
||||
error: () => {
|
||||
this.updatingStatus = false;
|
||||
this.errorMessage = 'Aggiornamento stato ordine non riuscito.';
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
downloadItemFile(itemId: string, filename: string): void {
|
||||
@@ -156,14 +182,16 @@ export class AdminDashboardComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
this.adminOrdersService.downloadOrderItemFile(this.selectedOrder.id, itemId).subscribe({
|
||||
next: (blob) => {
|
||||
this.downloadBlob(blob, filename || `order-item-${itemId}`);
|
||||
},
|
||||
error: () => {
|
||||
this.errorMessage = 'Download file non riuscito.';
|
||||
}
|
||||
});
|
||||
this.adminOrdersService
|
||||
.downloadOrderItemFile(this.selectedOrder.id, itemId)
|
||||
.subscribe({
|
||||
next: (blob) => {
|
||||
this.downloadBlob(blob, filename || `order-item-${itemId}`);
|
||||
},
|
||||
error: () => {
|
||||
this.errorMessage = 'Download file non riuscito.';
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
downloadConfirmation(): void {
|
||||
@@ -171,14 +199,19 @@ export class AdminDashboardComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
this.adminOrdersService.downloadOrderConfirmation(this.selectedOrder.id).subscribe({
|
||||
next: (blob) => {
|
||||
this.downloadBlob(blob, `conferma-${this.selectedOrder?.orderNumber || this.selectedOrder?.id}.pdf`);
|
||||
},
|
||||
error: () => {
|
||||
this.errorMessage = 'Download conferma ordine non riuscito.';
|
||||
}
|
||||
});
|
||||
this.adminOrdersService
|
||||
.downloadOrderConfirmation(this.selectedOrder.id)
|
||||
.subscribe({
|
||||
next: (blob) => {
|
||||
this.downloadBlob(
|
||||
blob,
|
||||
`conferma-${this.selectedOrder?.orderNumber || this.selectedOrder?.id}.pdf`,
|
||||
);
|
||||
},
|
||||
error: () => {
|
||||
this.errorMessage = 'Download conferma ordine non riuscito.';
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
downloadInvoice(): void {
|
||||
@@ -186,14 +219,19 @@ export class AdminDashboardComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
this.adminOrdersService.downloadOrderInvoice(this.selectedOrder.id).subscribe({
|
||||
next: (blob) => {
|
||||
this.downloadBlob(blob, `fattura-${this.selectedOrder?.orderNumber || this.selectedOrder?.id}.pdf`);
|
||||
},
|
||||
error: () => {
|
||||
this.errorMessage = 'Download fattura non riuscito.';
|
||||
}
|
||||
});
|
||||
this.adminOrdersService
|
||||
.downloadOrderInvoice(this.selectedOrder.id)
|
||||
.subscribe({
|
||||
next: (blob) => {
|
||||
this.downloadBlob(
|
||||
blob,
|
||||
`fattura-${this.selectedOrder?.orderNumber || this.selectedOrder?.id}.pdf`,
|
||||
);
|
||||
},
|
||||
error: () => {
|
||||
this.errorMessage = 'Download fattura non riuscito.';
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onStatusChange(event: Event): void {
|
||||
@@ -228,7 +266,10 @@ export class AdminDashboardComponent implements OnInit {
|
||||
}
|
||||
|
||||
isHexColor(value?: string): boolean {
|
||||
return typeof value === 'string' && /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value);
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value)
|
||||
);
|
||||
}
|
||||
|
||||
isSelected(orderId: string): boolean {
|
||||
@@ -236,11 +277,14 @@ export class AdminDashboardComponent implements OnInit {
|
||||
}
|
||||
|
||||
private applyOrderUpdate(updatedOrder: AdminOrder): void {
|
||||
this.orders = this.orders.map((order) => order.id === updatedOrder.id ? updatedOrder : order);
|
||||
this.orders = this.orders.map((order) =>
|
||||
order.id === updatedOrder.id ? updatedOrder : order,
|
||||
);
|
||||
this.applyListFiltersAndSelection();
|
||||
this.selectedOrder = updatedOrder;
|
||||
this.selectedStatus = updatedOrder.status;
|
||||
this.selectedPaymentMethod = updatedOrder.paymentMethod || this.selectedPaymentMethod;
|
||||
this.selectedPaymentMethod =
|
||||
updatedOrder.paymentMethod || this.selectedPaymentMethod;
|
||||
}
|
||||
|
||||
private applyListFiltersAndSelection(): void {
|
||||
@@ -252,7 +296,10 @@ export class AdminDashboardComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.selectedOrder || !this.filteredOrders.some(order => order.id === this.selectedOrder?.id)) {
|
||||
if (
|
||||
!this.selectedOrder ||
|
||||
!this.filteredOrders.some((order) => order.id === this.selectedOrder?.id)
|
||||
) {
|
||||
this.openDetails(this.filteredOrders[0].id);
|
||||
}
|
||||
}
|
||||
@@ -265,9 +312,14 @@ export class AdminDashboardComponent implements OnInit {
|
||||
const paymentStatus = (order.paymentStatus || 'PENDING').toUpperCase();
|
||||
const orderStatus = (order.status || '').toUpperCase();
|
||||
|
||||
const matchesSearch = !term || fullUuid.includes(term) || shortUuid.includes(term);
|
||||
const matchesPayment = this.paymentStatusFilter === 'ALL' || paymentStatus === this.paymentStatusFilter;
|
||||
const matchesOrderStatus = this.orderStatusFilter === 'ALL' || orderStatus === this.orderStatusFilter;
|
||||
const matchesSearch =
|
||||
!term || fullUuid.includes(term) || shortUuid.includes(term);
|
||||
const matchesPayment =
|
||||
this.paymentStatusFilter === 'ALL' ||
|
||||
paymentStatus === this.paymentStatusFilter;
|
||||
const matchesOrderStatus =
|
||||
this.orderStatusFilter === 'ALL' ||
|
||||
orderStatus === this.orderStatusFilter;
|
||||
|
||||
return matchesSearch && matchesPayment && matchesOrderStatus;
|
||||
});
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
<h2>Stock filamenti</h2>
|
||||
<p>Gestione materiali, varianti e stock per il calcolatore.</p>
|
||||
</div>
|
||||
<button type="button" (click)="loadData()" [disabled]="loading">Aggiorna</button>
|
||||
<button type="button" (click)="loadData()" [disabled]="loading">
|
||||
Aggiorna
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="alerts">
|
||||
@@ -16,8 +18,12 @@
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<h3>Inserimento rapido</h3>
|
||||
<button type="button" class="panel-toggle" (click)="toggleQuickInsertCollapsed()">
|
||||
{{ quickInsertCollapsed ? 'Espandi' : 'Collassa' }}
|
||||
<button
|
||||
type="button"
|
||||
class="panel-toggle"
|
||||
(click)="toggleQuickInsertCollapsed()"
|
||||
>
|
||||
{{ quickInsertCollapsed ? "Espandi" : "Collassa" }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -28,7 +34,11 @@
|
||||
<div class="form-grid">
|
||||
<label class="form-field form-field--wide">
|
||||
<span>Codice materiale</span>
|
||||
<input type="text" [(ngModel)]="newMaterial.materialCode" placeholder="PLA, PETG, TPU..." />
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="newMaterial.materialCode"
|
||||
placeholder="PLA, PETG, TPU..."
|
||||
/>
|
||||
</label>
|
||||
<label class="form-field form-field--wide">
|
||||
<span>Etichetta tecnico</span>
|
||||
@@ -52,8 +62,12 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="button" (click)="createMaterial()" [disabled]="creatingMaterial">
|
||||
{{ creatingMaterial ? 'Salvataggio...' : 'Aggiungi materiale' }}
|
||||
<button
|
||||
type="button"
|
||||
(click)="createMaterial()"
|
||||
[disabled]="creatingMaterial"
|
||||
>
|
||||
{{ creatingMaterial ? "Salvataggio..." : "Aggiungi materiale" }}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
@@ -63,22 +77,37 @@
|
||||
<label class="form-field">
|
||||
<span>Materiale</span>
|
||||
<select [(ngModel)]="newVariant.materialTypeId">
|
||||
<option *ngFor="let material of materials; trackBy: trackById" [ngValue]="material.id">
|
||||
<option
|
||||
*ngFor="let material of materials; trackBy: trackById"
|
||||
[ngValue]="material.id"
|
||||
>
|
||||
{{ material.materialCode }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Nome variante</span>
|
||||
<input type="text" [(ngModel)]="newVariant.variantDisplayName" placeholder="PLA Nero Opaco BrandX" />
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="newVariant.variantDisplayName"
|
||||
placeholder="PLA Nero Opaco BrandX"
|
||||
/>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Colore</span>
|
||||
<input type="text" [(ngModel)]="newVariant.colorName" placeholder="Nero, Bianco..." />
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="newVariant.colorName"
|
||||
placeholder="Nero, Bianco..."
|
||||
/>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Hex colore</span>
|
||||
<input type="text" [(ngModel)]="newVariant.colorHex" placeholder="#1A1A1A" />
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="newVariant.colorHex"
|
||||
placeholder="#1A1A1A"
|
||||
/>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Finitura</span>
|
||||
@@ -93,19 +122,40 @@
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Brand</span>
|
||||
<input type="text" [(ngModel)]="newVariant.brand" placeholder="Bambu, SUNLU..." />
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="newVariant.brand"
|
||||
placeholder="Bambu, SUNLU..."
|
||||
/>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Costo CHF/kg</span>
|
||||
<input type="number" step="0.01" min="0" [(ngModel)]="newVariant.costChfPerKg" />
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
[(ngModel)]="newVariant.costChfPerKg"
|
||||
/>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Stock spool</span>
|
||||
<input type="number" step="0.001" min="0" max="999.999" [(ngModel)]="newVariant.stockSpools" />
|
||||
<input
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
max="999.999"
|
||||
[(ngModel)]="newVariant.stockSpools"
|
||||
/>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Spool netto kg</span>
|
||||
<input type="number" step="0.001" min="0.001" max="999.999" [(ngModel)]="newVariant.spoolNetKg" />
|
||||
<input
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0.001"
|
||||
max="999.999"
|
||||
[(ngModel)]="newVariant.spoolNetKg"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -125,12 +175,26 @@
|
||||
</div>
|
||||
|
||||
<p class="variant-meta">
|
||||
Stock spools: <strong>{{ newVariant.stockSpools | number:'1.0-3' }}</strong> |
|
||||
Filamento totale: <strong>{{ computeStockFilamentGrams(newVariant.stockSpools, newVariant.spoolNetKg) | number:'1.0-0' }} g</strong>
|
||||
Stock spools:
|
||||
<strong>{{ newVariant.stockSpools | number: "1.0-3" }}</strong> |
|
||||
Filamento totale:
|
||||
<strong
|
||||
>{{
|
||||
computeStockFilamentGrams(
|
||||
newVariant.stockSpools,
|
||||
newVariant.spoolNetKg
|
||||
) | number: "1.0-0"
|
||||
}}
|
||||
g</strong
|
||||
>
|
||||
</p>
|
||||
|
||||
<button type="button" (click)="createVariant()" [disabled]="creatingVariant || !materials.length">
|
||||
{{ creatingVariant ? 'Salvataggio...' : 'Aggiungi variante' }}
|
||||
<button
|
||||
type="button"
|
||||
(click)="createVariant()"
|
||||
[disabled]="creatingVariant || !materials.length"
|
||||
>
|
||||
{{ creatingVariant ? "Salvataggio..." : "Aggiungi variante" }}
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
@@ -140,37 +204,68 @@
|
||||
<section class="panel">
|
||||
<h3>Varianti filamento</h3>
|
||||
<div class="variant-list">
|
||||
<article class="variant-row" *ngFor="let variant of variants; trackBy: trackById">
|
||||
<article
|
||||
class="variant-row"
|
||||
*ngFor="let variant of variants; trackBy: trackById"
|
||||
>
|
||||
<div class="variant-header">
|
||||
<button
|
||||
type="button"
|
||||
class="expand-toggle"
|
||||
(click)="toggleVariantExpanded(variant.id)"
|
||||
[attr.aria-expanded]="isVariantExpanded(variant.id)">
|
||||
{{ isVariantExpanded(variant.id) ? '▾' : '▸' }}
|
||||
[attr.aria-expanded]="isVariantExpanded(variant.id)"
|
||||
>
|
||||
{{ isVariantExpanded(variant.id) ? "▾" : "▸" }}
|
||||
</button>
|
||||
|
||||
<div class="variant-head-main">
|
||||
<strong>{{ variant.variantDisplayName }}</strong>
|
||||
<div class="variant-collapsed-summary" *ngIf="!isVariantExpanded(variant.id)">
|
||||
<div
|
||||
class="variant-collapsed-summary"
|
||||
*ngIf="!isVariantExpanded(variant.id)"
|
||||
>
|
||||
<span class="color-summary">
|
||||
<span class="color-dot" [style.background-color]="getVariantColorHex(variant)"></span>
|
||||
{{ variant.colorName || 'N/D' }}
|
||||
<span
|
||||
class="color-dot"
|
||||
[style.background-color]="getVariantColorHex(variant)"
|
||||
></span>
|
||||
{{ variant.colorName || "N/D" }}
|
||||
</span>
|
||||
<span>Stock spools: {{ variant.stockSpools | number:'1.0-3' }}</span>
|
||||
<span>Filamento: {{ computeStockFilamentGrams(variant.stockSpools, variant.spoolNetKg) | number:'1.0-0' }} g</span>
|
||||
<span
|
||||
>Stock spools:
|
||||
{{ variant.stockSpools | number: "1.0-3" }}</span
|
||||
>
|
||||
<span
|
||||
>Filamento:
|
||||
{{
|
||||
computeStockFilamentGrams(
|
||||
variant.stockSpools,
|
||||
variant.spoolNetKg
|
||||
) | number: "1.0-0"
|
||||
}}
|
||||
g</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="variant-head-actions">
|
||||
<span class="badge low" *ngIf="isLowStock(variant)">Stock basso</span>
|
||||
<span class="badge ok" *ngIf="!isLowStock(variant)">Stock ok</span>
|
||||
<span class="badge low" *ngIf="isLowStock(variant)"
|
||||
>Stock basso</span
|
||||
>
|
||||
<span class="badge ok" *ngIf="!isLowStock(variant)"
|
||||
>Stock ok</span
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-delete"
|
||||
(click)="openDeleteVariant(variant)"
|
||||
[disabled]="deletingVariantIds.has(variant.id)">
|
||||
{{ deletingVariantIds.has(variant.id) ? 'Eliminazione...' : 'Elimina' }}
|
||||
[disabled]="deletingVariantIds.has(variant.id)"
|
||||
>
|
||||
{{
|
||||
deletingVariantIds.has(variant.id)
|
||||
? "Eliminazione..."
|
||||
: "Elimina"
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -179,7 +274,10 @@
|
||||
<label class="form-field">
|
||||
<span>Materiale</span>
|
||||
<select [(ngModel)]="variant.materialTypeId">
|
||||
<option *ngFor="let material of materials; trackBy: trackById" [ngValue]="material.id">
|
||||
<option
|
||||
*ngFor="let material of materials; trackBy: trackById"
|
||||
[ngValue]="material.id"
|
||||
>
|
||||
{{ material.materialCode }}
|
||||
</option>
|
||||
</select>
|
||||
@@ -213,15 +311,32 @@
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Costo CHF/kg</span>
|
||||
<input type="number" step="0.01" min="0" [(ngModel)]="variant.costChfPerKg" />
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
[(ngModel)]="variant.costChfPerKg"
|
||||
/>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Stock spool</span>
|
||||
<input type="number" step="0.001" min="0" max="999.999" [(ngModel)]="variant.stockSpools" />
|
||||
<input
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
max="999.999"
|
||||
[(ngModel)]="variant.stockSpools"
|
||||
/>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Spool netto kg</span>
|
||||
<input type="number" step="0.001" min="0.001" max="999.999" [(ngModel)]="variant.spoolNetKg" />
|
||||
<input
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0.001"
|
||||
max="999.999"
|
||||
[(ngModel)]="variant.spoolNetKg"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -241,33 +356,57 @@
|
||||
</div>
|
||||
|
||||
<p class="variant-meta" *ngIf="isVariantExpanded(variant.id)">
|
||||
Stock spools: <strong>{{ variant.stockSpools | number:'1.0-3' }}</strong> |
|
||||
Filamento totale: <strong>{{ computeStockFilamentGrams(variant.stockSpools, variant.spoolNetKg) | number:'1.0-0' }} g</strong>
|
||||
Stock spools:
|
||||
<strong>{{ variant.stockSpools | number: "1.0-3" }}</strong> |
|
||||
Filamento totale:
|
||||
<strong
|
||||
>{{
|
||||
computeStockFilamentGrams(
|
||||
variant.stockSpools,
|
||||
variant.spoolNetKg
|
||||
) | number: "1.0-0"
|
||||
}}
|
||||
g</strong
|
||||
>
|
||||
</p>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
*ngIf="isVariantExpanded(variant.id)"
|
||||
(click)="saveVariant(variant)"
|
||||
[disabled]="savingVariantIds.has(variant.id)">
|
||||
{{ savingVariantIds.has(variant.id) ? 'Salvataggio...' : 'Salva variante' }}
|
||||
[disabled]="savingVariantIds.has(variant.id)"
|
||||
>
|
||||
{{
|
||||
savingVariantIds.has(variant.id)
|
||||
? "Salvataggio..."
|
||||
: "Salva variante"
|
||||
}}
|
||||
</button>
|
||||
</article>
|
||||
</div>
|
||||
<p class="muted" *ngIf="variants.length === 0">Nessuna variante configurata.</p>
|
||||
<p class="muted" *ngIf="variants.length === 0">
|
||||
Nessuna variante configurata.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<h3>Materiali</h3>
|
||||
<button type="button" class="panel-toggle" (click)="toggleMaterialsCollapsed()">
|
||||
{{ materialsCollapsed ? 'Espandi' : 'Collassa' }}
|
||||
<button
|
||||
type="button"
|
||||
class="panel-toggle"
|
||||
(click)="toggleMaterialsCollapsed()"
|
||||
>
|
||||
{{ materialsCollapsed ? "Espandi" : "Collassa" }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!materialsCollapsed; else materialsCollapsedTpl">
|
||||
<div class="material-grid">
|
||||
<article class="material-card" *ngFor="let material of materials; trackBy: trackById">
|
||||
<article
|
||||
class="material-card"
|
||||
*ngFor="let material of materials; trackBy: trackById"
|
||||
>
|
||||
<div class="form-grid">
|
||||
<label class="form-field form-field--wide">
|
||||
<span>Codice</span>
|
||||
@@ -275,7 +414,11 @@
|
||||
</label>
|
||||
<label class="form-field form-field--wide">
|
||||
<span>Etichetta tecnico</span>
|
||||
<input type="text" [(ngModel)]="material.technicalTypeLabel" [disabled]="!material.isTechnical" />
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="material.technicalTypeLabel"
|
||||
[disabled]="!material.isTechnical"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -290,12 +433,22 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="button" (click)="saveMaterial(material)" [disabled]="savingMaterialIds.has(material.id)">
|
||||
{{ savingMaterialIds.has(material.id) ? 'Salvataggio...' : 'Salva materiale' }}
|
||||
<button
|
||||
type="button"
|
||||
(click)="saveMaterial(material)"
|
||||
[disabled]="savingMaterialIds.has(material.id)"
|
||||
>
|
||||
{{
|
||||
savingMaterialIds.has(material.id)
|
||||
? "Salvataggio..."
|
||||
: "Salva materiale"
|
||||
}}
|
||||
</button>
|
||||
</article>
|
||||
</div>
|
||||
<p class="muted" *ngIf="materials.length === 0">Nessun materiale configurato.</p>
|
||||
<p class="muted" *ngIf="materials.length === 0">
|
||||
Nessun materiale configurato.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -313,19 +466,38 @@
|
||||
<p class="muted">Sezione collassata.</p>
|
||||
</ng-template>
|
||||
|
||||
<div class="dialog-backdrop" *ngIf="variantToDelete" (click)="closeDeleteVariantDialog()"></div>
|
||||
<div
|
||||
class="dialog-backdrop"
|
||||
*ngIf="variantToDelete"
|
||||
(click)="closeDeleteVariantDialog()"
|
||||
></div>
|
||||
<div class="confirm-dialog" *ngIf="variantToDelete">
|
||||
<h4>Sei sicuro?</h4>
|
||||
<p>Vuoi eliminare la variante <strong>{{ variantToDelete.variantDisplayName }}</strong>?</p>
|
||||
<p>
|
||||
Vuoi eliminare la variante
|
||||
<strong>{{ variantToDelete.variantDisplayName }}</strong
|
||||
>?
|
||||
</p>
|
||||
<p class="muted">L'operazione non è reversibile.</p>
|
||||
<div class="dialog-actions">
|
||||
<button type="button" class="btn-secondary" (click)="closeDeleteVariantDialog()">Annulla</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-secondary"
|
||||
(click)="closeDeleteVariantDialog()"
|
||||
>
|
||||
Annulla
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-delete"
|
||||
(click)="confirmDeleteVariant()"
|
||||
[disabled]="variantToDelete && deletingVariantIds.has(variantToDelete.id)">
|
||||
{{ variantToDelete && deletingVariantIds.has(variantToDelete.id) ? 'Eliminazione...' : 'Conferma elimina' }}
|
||||
[disabled]="variantToDelete && deletingVariantIds.has(variantToDelete.id)"
|
||||
>
|
||||
{{
|
||||
variantToDelete && deletingVariantIds.has(variantToDelete.id)
|
||||
? "Eliminazione..."
|
||||
: "Conferma elimina"
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
AdminFilamentVariant,
|
||||
AdminOperationsService,
|
||||
AdminUpsertFilamentMaterialTypePayload,
|
||||
AdminUpsertFilamentVariantPayload
|
||||
AdminUpsertFilamentVariantPayload,
|
||||
} from '../services/admin-operations.service';
|
||||
import { forkJoin } from 'rxjs';
|
||||
import { getColorHex } from '../../../core/constants/colors.const';
|
||||
@@ -16,7 +16,7 @@ import { getColorHex } from '../../../core/constants/colors.const';
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule],
|
||||
templateUrl: './admin-filament-stock.component.html',
|
||||
styleUrl: './admin-filament-stock.component.scss'
|
||||
styleUrl: './admin-filament-stock.component.scss',
|
||||
})
|
||||
export class AdminFilamentStockComponent implements OnInit {
|
||||
private readonly adminOperationsService = inject(AdminOperationsService);
|
||||
@@ -40,7 +40,7 @@ export class AdminFilamentStockComponent implements OnInit {
|
||||
materialCode: '',
|
||||
isFlexible: false,
|
||||
isTechnical: false,
|
||||
technicalTypeLabel: ''
|
||||
technicalTypeLabel: '',
|
||||
};
|
||||
|
||||
newVariant: AdminUpsertFilamentVariantPayload = {
|
||||
@@ -55,7 +55,7 @@ export class AdminFilamentStockComponent implements OnInit {
|
||||
costChfPerKg: 0,
|
||||
stockSpools: 0,
|
||||
spoolNetKg: 1,
|
||||
isActive: true
|
||||
isActive: true,
|
||||
};
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -69,13 +69,13 @@ export class AdminFilamentStockComponent implements OnInit {
|
||||
|
||||
forkJoin({
|
||||
materials: this.adminOperationsService.getFilamentMaterials(),
|
||||
variants: this.adminOperationsService.getFilamentVariants()
|
||||
variants: this.adminOperationsService.getFilamentVariants(),
|
||||
}).subscribe({
|
||||
next: ({ materials, variants }) => {
|
||||
this.materials = this.sortMaterials(materials);
|
||||
this.variants = this.sortVariants(variants);
|
||||
const existingIds = new Set(this.variants.map(v => v.id));
|
||||
this.expandedVariantIds.forEach(id => {
|
||||
const existingIds = new Set(this.variants.map((v) => v.id));
|
||||
this.expandedVariantIds.forEach((id) => {
|
||||
if (!existingIds.has(id)) {
|
||||
this.expandedVariantIds.delete(id);
|
||||
}
|
||||
@@ -87,8 +87,11 @@ export class AdminFilamentStockComponent implements OnInit {
|
||||
},
|
||||
error: (err) => {
|
||||
this.loading = false;
|
||||
this.errorMessage = this.extractErrorMessage(err, 'Impossibile caricare i filamenti.');
|
||||
}
|
||||
this.errorMessage = this.extractErrorMessage(
|
||||
err,
|
||||
'Impossibile caricare i filamenti.',
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -107,7 +110,7 @@ export class AdminFilamentStockComponent implements OnInit {
|
||||
isTechnical: !!this.newMaterial.isTechnical,
|
||||
technicalTypeLabel: this.newMaterial.isTechnical
|
||||
? (this.newMaterial.technicalTypeLabel || '').trim()
|
||||
: ''
|
||||
: '',
|
||||
};
|
||||
|
||||
this.adminOperationsService.createFilamentMaterial(payload).subscribe({
|
||||
@@ -120,15 +123,18 @@ export class AdminFilamentStockComponent implements OnInit {
|
||||
materialCode: '',
|
||||
isFlexible: false,
|
||||
isTechnical: false,
|
||||
technicalTypeLabel: ''
|
||||
technicalTypeLabel: '',
|
||||
};
|
||||
this.creatingMaterial = false;
|
||||
this.successMessage = 'Materiale aggiunto.';
|
||||
},
|
||||
error: (err) => {
|
||||
this.creatingMaterial = false;
|
||||
this.errorMessage = this.extractErrorMessage(err, 'Creazione materiale non riuscita.');
|
||||
}
|
||||
this.errorMessage = this.extractErrorMessage(
|
||||
err,
|
||||
'Creazione materiale non riuscita.',
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -145,34 +151,41 @@ export class AdminFilamentStockComponent implements OnInit {
|
||||
materialCode: (material.materialCode || '').trim(),
|
||||
isFlexible: !!material.isFlexible,
|
||||
isTechnical: !!material.isTechnical,
|
||||
technicalTypeLabel: material.isTechnical ? (material.technicalTypeLabel || '').trim() : ''
|
||||
technicalTypeLabel: material.isTechnical
|
||||
? (material.technicalTypeLabel || '').trim()
|
||||
: '',
|
||||
};
|
||||
|
||||
this.adminOperationsService.updateFilamentMaterial(material.id, payload).subscribe({
|
||||
next: (updated) => {
|
||||
this.materials = this.sortMaterials(
|
||||
this.materials.map((m) => (m.id === updated.id ? updated : m))
|
||||
);
|
||||
this.variants = this.variants.map((variant) => {
|
||||
if (variant.materialTypeId !== updated.id) {
|
||||
return variant;
|
||||
}
|
||||
return {
|
||||
...variant,
|
||||
materialCode: updated.materialCode,
|
||||
materialIsFlexible: updated.isFlexible,
|
||||
materialIsTechnical: updated.isTechnical,
|
||||
materialTechnicalTypeLabel: updated.technicalTypeLabel
|
||||
};
|
||||
});
|
||||
this.savingMaterialIds.delete(material.id);
|
||||
this.successMessage = 'Materiale aggiornato.';
|
||||
},
|
||||
error: (err) => {
|
||||
this.savingMaterialIds.delete(material.id);
|
||||
this.errorMessage = this.extractErrorMessage(err, 'Aggiornamento materiale non riuscito.');
|
||||
}
|
||||
});
|
||||
this.adminOperationsService
|
||||
.updateFilamentMaterial(material.id, payload)
|
||||
.subscribe({
|
||||
next: (updated) => {
|
||||
this.materials = this.sortMaterials(
|
||||
this.materials.map((m) => (m.id === updated.id ? updated : m)),
|
||||
);
|
||||
this.variants = this.variants.map((variant) => {
|
||||
if (variant.materialTypeId !== updated.id) {
|
||||
return variant;
|
||||
}
|
||||
return {
|
||||
...variant,
|
||||
materialCode: updated.materialCode,
|
||||
materialIsFlexible: updated.isFlexible,
|
||||
materialIsTechnical: updated.isTechnical,
|
||||
materialTechnicalTypeLabel: updated.technicalTypeLabel,
|
||||
};
|
||||
});
|
||||
this.savingMaterialIds.delete(material.id);
|
||||
this.successMessage = 'Materiale aggiornato.';
|
||||
},
|
||||
error: (err) => {
|
||||
this.savingMaterialIds.delete(material.id);
|
||||
this.errorMessage = this.extractErrorMessage(
|
||||
err,
|
||||
'Aggiornamento materiale non riuscito.',
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
createVariant(): void {
|
||||
@@ -189,7 +202,8 @@ export class AdminFilamentStockComponent implements OnInit {
|
||||
next: (created) => {
|
||||
this.variants = this.sortVariants([...this.variants, created]);
|
||||
this.newVariant = {
|
||||
materialTypeId: this.newVariant.materialTypeId || this.materials[0]?.id || 0,
|
||||
materialTypeId:
|
||||
this.newVariant.materialTypeId || this.materials[0]?.id || 0,
|
||||
variantDisplayName: '',
|
||||
colorName: '',
|
||||
colorHex: '',
|
||||
@@ -200,15 +214,18 @@ export class AdminFilamentStockComponent implements OnInit {
|
||||
costChfPerKg: 0,
|
||||
stockSpools: 0,
|
||||
spoolNetKg: 1,
|
||||
isActive: true
|
||||
isActive: true,
|
||||
};
|
||||
this.creatingVariant = false;
|
||||
this.successMessage = 'Variante aggiunta.';
|
||||
},
|
||||
error: (err) => {
|
||||
this.creatingVariant = false;
|
||||
this.errorMessage = this.extractErrorMessage(err, 'Creazione variante non riuscita.');
|
||||
}
|
||||
this.errorMessage = this.extractErrorMessage(
|
||||
err,
|
||||
'Creazione variante non riuscita.',
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -222,30 +239,43 @@ export class AdminFilamentStockComponent implements OnInit {
|
||||
this.savingVariantIds.add(variant.id);
|
||||
|
||||
const payload = this.toVariantPayload(variant);
|
||||
this.adminOperationsService.updateFilamentVariant(variant.id, payload).subscribe({
|
||||
next: (updated) => {
|
||||
this.variants = this.sortVariants(
|
||||
this.variants.map((v) => (v.id === updated.id ? updated : v))
|
||||
);
|
||||
this.savingVariantIds.delete(variant.id);
|
||||
this.successMessage = 'Variante aggiornata.';
|
||||
},
|
||||
error: (err) => {
|
||||
this.savingVariantIds.delete(variant.id);
|
||||
this.errorMessage = this.extractErrorMessage(err, 'Aggiornamento variante non riuscito.');
|
||||
}
|
||||
});
|
||||
this.adminOperationsService
|
||||
.updateFilamentVariant(variant.id, payload)
|
||||
.subscribe({
|
||||
next: (updated) => {
|
||||
this.variants = this.sortVariants(
|
||||
this.variants.map((v) => (v.id === updated.id ? updated : v)),
|
||||
);
|
||||
this.savingVariantIds.delete(variant.id);
|
||||
this.successMessage = 'Variante aggiornata.';
|
||||
},
|
||||
error: (err) => {
|
||||
this.savingVariantIds.delete(variant.id);
|
||||
this.errorMessage = this.extractErrorMessage(
|
||||
err,
|
||||
'Aggiornamento variante non riuscito.',
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
isLowStock(variant: AdminFilamentVariant): boolean {
|
||||
return this.computeStockFilamentGrams(variant.stockSpools, variant.spoolNetKg) < 1000;
|
||||
return (
|
||||
this.computeStockFilamentGrams(variant.stockSpools, variant.spoolNetKg) <
|
||||
1000
|
||||
);
|
||||
}
|
||||
|
||||
computeStockKg(stockSpools?: number, spoolNetKg?: number): number {
|
||||
const spools = Number(stockSpools ?? 0);
|
||||
const netKg = Number(spoolNetKg ?? 0);
|
||||
|
||||
if (!Number.isFinite(spools) || !Number.isFinite(netKg) || spools < 0 || netKg < 0) {
|
||||
if (
|
||||
!Number.isFinite(spools) ||
|
||||
!Number.isFinite(netKg) ||
|
||||
spools < 0 ||
|
||||
netKg < 0
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
return spools * netKg;
|
||||
@@ -298,7 +328,7 @@ export class AdminFilamentStockComponent implements OnInit {
|
||||
|
||||
this.adminOperationsService.deleteFilamentVariant(variant.id).subscribe({
|
||||
next: () => {
|
||||
this.variants = this.variants.filter(v => v.id !== variant.id);
|
||||
this.variants = this.variants.filter((v) => v.id !== variant.id);
|
||||
this.expandedVariantIds.delete(variant.id);
|
||||
this.deletingVariantIds.delete(variant.id);
|
||||
this.variantToDelete = null;
|
||||
@@ -306,8 +336,11 @@ export class AdminFilamentStockComponent implements OnInit {
|
||||
},
|
||||
error: (err) => {
|
||||
this.deletingVariantIds.delete(variant.id);
|
||||
this.errorMessage = this.extractErrorMessage(err, 'Eliminazione variante non riuscita.');
|
||||
}
|
||||
this.errorMessage = this.extractErrorMessage(
|
||||
err,
|
||||
'Eliminazione variante non riuscita.',
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -319,7 +352,9 @@ export class AdminFilamentStockComponent implements OnInit {
|
||||
this.quickInsertCollapsed = !this.quickInsertCollapsed;
|
||||
}
|
||||
|
||||
private toVariantPayload(source: AdminUpsertFilamentVariantPayload | AdminFilamentVariant): AdminUpsertFilamentVariantPayload {
|
||||
private toVariantPayload(
|
||||
source: AdminUpsertFilamentVariantPayload | AdminFilamentVariant,
|
||||
): AdminUpsertFilamentVariantPayload {
|
||||
return {
|
||||
materialTypeId: Number(source.materialTypeId),
|
||||
variantDisplayName: (source.variantDisplayName || '').trim(),
|
||||
@@ -332,21 +367,31 @@ export class AdminFilamentStockComponent implements OnInit {
|
||||
costChfPerKg: Number(source.costChfPerKg ?? 0),
|
||||
stockSpools: Number(source.stockSpools ?? 0),
|
||||
spoolNetKg: Number(source.spoolNetKg ?? 0),
|
||||
isActive: source.isActive !== false
|
||||
isActive: source.isActive !== false,
|
||||
};
|
||||
}
|
||||
|
||||
private sortMaterials(materials: AdminFilamentMaterialType[]): AdminFilamentMaterialType[] {
|
||||
return [...materials].sort((a, b) => a.materialCode.localeCompare(b.materialCode));
|
||||
private sortMaterials(
|
||||
materials: AdminFilamentMaterialType[],
|
||||
): AdminFilamentMaterialType[] {
|
||||
return [...materials].sort((a, b) =>
|
||||
a.materialCode.localeCompare(b.materialCode),
|
||||
);
|
||||
}
|
||||
|
||||
private sortVariants(variants: AdminFilamentVariant[]): AdminFilamentVariant[] {
|
||||
private sortVariants(
|
||||
variants: AdminFilamentVariant[],
|
||||
): AdminFilamentVariant[] {
|
||||
return [...variants].sort((a, b) => {
|
||||
const byMaterial = (a.materialCode || '').localeCompare(b.materialCode || '');
|
||||
const byMaterial = (a.materialCode || '').localeCompare(
|
||||
b.materialCode || '',
|
||||
);
|
||||
if (byMaterial !== 0) {
|
||||
return byMaterial;
|
||||
}
|
||||
return (a.variantDisplayName || '').localeCompare(b.variantDisplayName || '');
|
||||
return (a.variantDisplayName || '').localeCompare(
|
||||
b.variantDisplayName || '',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,11 @@
|
||||
required
|
||||
/>
|
||||
|
||||
<button type="submit" [disabled]="loading || !password.trim() || lockSecondsRemaining > 0">
|
||||
{{ loading ? 'Accesso...' : 'Accedi' }}
|
||||
<button
|
||||
type="submit"
|
||||
[disabled]="loading || !password.trim() || lockSecondsRemaining > 0"
|
||||
>
|
||||
{{ loading ? "Accesso..." : "Accedi" }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -3,7 +3,10 @@ import { Component, inject, OnDestroy } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { AdminAuthResponse, AdminAuthService } from '../services/admin-auth.service';
|
||||
import {
|
||||
AdminAuthResponse,
|
||||
AdminAuthService,
|
||||
} from '../services/admin-auth.service';
|
||||
|
||||
const SUPPORTED_LANGS = new Set(['it', 'en', 'de', 'fr']);
|
||||
|
||||
@@ -12,7 +15,7 @@ const SUPPORTED_LANGS = new Set(['it', 'en', 'de', 'fr']);
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule],
|
||||
templateUrl: './admin-login.component.html',
|
||||
styleUrl: './admin-login.component.scss'
|
||||
styleUrl: './admin-login.component.scss',
|
||||
})
|
||||
export class AdminLoginComponent implements OnDestroy {
|
||||
private readonly authService = inject(AdminAuthService);
|
||||
@@ -26,7 +29,11 @@ export class AdminLoginComponent implements OnDestroy {
|
||||
private lockTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
submit(): void {
|
||||
if (!this.password.trim() || this.loading || this.lockSecondsRemaining > 0) {
|
||||
if (
|
||||
!this.password.trim() ||
|
||||
this.loading ||
|
||||
this.lockSecondsRemaining > 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -53,7 +60,7 @@ export class AdminLoginComponent implements OnDestroy {
|
||||
error: (error: HttpErrorResponse) => {
|
||||
this.loading = false;
|
||||
this.handleLoginFailure(this.extractRetryAfterSeconds(error));
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,14 @@
|
||||
<h2>Sessioni quote</h2>
|
||||
<p>Sessioni create dal configuratore con stato e conversione ordine.</p>
|
||||
</div>
|
||||
<button type="button" class="btn-primary" (click)="loadSessions()" [disabled]="loading">Aggiorna</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-primary"
|
||||
(click)="loadSessions()"
|
||||
[disabled]="loading"
|
||||
>
|
||||
Aggiorna
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<p class="error" *ngIf="errorMessage">{{ errorMessage }}</p>
|
||||
@@ -26,41 +33,73 @@
|
||||
<tbody>
|
||||
<ng-container *ngFor="let session of sessions">
|
||||
<tr>
|
||||
<td [title]="session.id">{{ session.id | slice:0:8 }}</td>
|
||||
<td>{{ session.createdAt | date:'short' }}</td>
|
||||
<td>{{ session.expiresAt | date:'short' }}</td>
|
||||
<td [title]="session.id">{{ session.id | slice: 0 : 8 }}</td>
|
||||
<td>{{ session.createdAt | date: "short" }}</td>
|
||||
<td>{{ session.expiresAt | date: "short" }}</td>
|
||||
<td>{{ session.materialCode }}</td>
|
||||
<td>{{ session.status }}</td>
|
||||
<td>{{ session.convertedOrderId || '-' }}</td>
|
||||
<td>{{ session.convertedOrderId || "-" }}</td>
|
||||
<td class="actions">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-secondary"
|
||||
(click)="toggleSessionDetail(session)">
|
||||
{{ isDetailOpen(session.id) ? 'Nascondi' : 'Vedi' }}
|
||||
(click)="toggleSessionDetail(session)"
|
||||
>
|
||||
{{ isDetailOpen(session.id) ? "Nascondi" : "Vedi" }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-danger"
|
||||
(click)="deleteSession(session)"
|
||||
[disabled]="isDeletingSession(session.id) || !!session.convertedOrderId"
|
||||
[title]="session.convertedOrderId ? 'Sessione collegata a un ordine, non eliminabile.' : ''">
|
||||
{{ isDeletingSession(session.id) ? 'Eliminazione...' : 'Elimina' }}
|
||||
[disabled]="
|
||||
isDeletingSession(session.id) || !!session.convertedOrderId
|
||||
"
|
||||
[title]="
|
||||
session.convertedOrderId
|
||||
? 'Sessione collegata a un ordine, non eliminabile.'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
{{
|
||||
isDeletingSession(session.id) ? "Eliminazione..." : "Elimina"
|
||||
}}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="isDetailOpen(session.id)">
|
||||
<td colspan="7" class="detail-cell">
|
||||
<div *ngIf="isLoadingDetail(session.id)">Caricamento dettaglio...</div>
|
||||
<div *ngIf="!isLoadingDetail(session.id) && getSessionDetail(session.id) as detail" class="detail-box">
|
||||
<div *ngIf="isLoadingDetail(session.id)">
|
||||
Caricamento dettaglio...
|
||||
</div>
|
||||
<div
|
||||
*ngIf="
|
||||
!isLoadingDetail(session.id) &&
|
||||
getSessionDetail(session.id) as detail
|
||||
"
|
||||
class="detail-box"
|
||||
>
|
||||
<div class="detail-summary">
|
||||
<div><strong>Elementi:</strong> {{ detail.items.length }}</div>
|
||||
<div><strong>Totale articoli:</strong> {{ detail.itemsTotalChf | currency:'CHF' }}</div>
|
||||
<div><strong>Spedizione:</strong> {{ detail.shippingCostChf | currency:'CHF' }}</div>
|
||||
<div><strong>Totale sessione:</strong> {{ detail.grandTotalChf | currency:'CHF' }}</div>
|
||||
<div>
|
||||
<strong>Elementi:</strong> {{ detail.items.length }}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Totale articoli:</strong>
|
||||
{{ detail.itemsTotalChf | currency: "CHF" }}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Spedizione:</strong>
|
||||
{{ detail.shippingCostChf | currency: "CHF" }}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Totale sessione:</strong>
|
||||
{{ detail.grandTotalChf | currency: "CHF" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="detail-table" *ngIf="detail.items.length > 0; else noItemsTpl">
|
||||
<table
|
||||
class="detail-table"
|
||||
*ngIf="detail.items.length > 0; else noItemsTpl"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>File</th>
|
||||
@@ -76,9 +115,15 @@
|
||||
<td>{{ item.originalFilename }}</td>
|
||||
<td>{{ item.quantity }}</td>
|
||||
<td>{{ formatPrintTime(item.printTimeSeconds) }}</td>
|
||||
<td>{{ item.materialGrams ? (item.materialGrams | number:'1.0-2') + ' g' : '-' }}</td>
|
||||
<td>
|
||||
{{
|
||||
item.materialGrams
|
||||
? (item.materialGrams | number: "1.0-2") + " g"
|
||||
: "-"
|
||||
}}
|
||||
</td>
|
||||
<td>{{ item.status }}</td>
|
||||
<td>{{ item.unitPriceChf | currency:'CHF' }}</td>
|
||||
<td>{{ item.unitPriceChf | currency: "CHF" }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Component, inject, OnInit } from '@angular/core';
|
||||
import {
|
||||
AdminOperationsService,
|
||||
AdminQuoteSession,
|
||||
AdminQuoteSessionDetail
|
||||
AdminQuoteSessionDetail,
|
||||
} from '../services/admin-operations.service';
|
||||
|
||||
@Component({
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './admin-sessions.component.html',
|
||||
styleUrl: './admin-sessions.component.scss'
|
||||
styleUrl: './admin-sessions.component.scss',
|
||||
})
|
||||
export class AdminSessionsComponent implements OnInit {
|
||||
private readonly adminOperationsService = inject(AdminOperationsService);
|
||||
@@ -41,7 +41,7 @@ export class AdminSessionsComponent implements OnInit {
|
||||
error: () => {
|
||||
this.loading = false;
|
||||
this.errorMessage = 'Impossibile caricare le sessioni.';
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ export class AdminSessionsComponent implements OnInit {
|
||||
}
|
||||
|
||||
const confirmed = window.confirm(
|
||||
`Vuoi eliminare la sessione ${session.id}? Questa azione non si puo annullare.`
|
||||
`Vuoi eliminare la sessione ${session.id}? Questa azione non si puo annullare.`,
|
||||
);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
@@ -69,8 +69,11 @@ export class AdminSessionsComponent implements OnInit {
|
||||
},
|
||||
error: (err) => {
|
||||
this.deletingSessionIds.delete(session.id);
|
||||
this.errorMessage = this.extractErrorMessage(err, 'Impossibile eliminare la sessione.');
|
||||
}
|
||||
this.errorMessage = this.extractErrorMessage(
|
||||
err,
|
||||
'Impossibile eliminare la sessione.',
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -85,7 +88,10 @@ export class AdminSessionsComponent implements OnInit {
|
||||
}
|
||||
|
||||
this.expandedSessionId = session.id;
|
||||
if (this.sessionDetailsById[session.id] || this.loadingDetailSessionIds.has(session.id)) {
|
||||
if (
|
||||
this.sessionDetailsById[session.id] ||
|
||||
this.loadingDetailSessionIds.has(session.id)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -94,14 +100,17 @@ export class AdminSessionsComponent implements OnInit {
|
||||
next: (detail) => {
|
||||
this.sessionDetailsById = {
|
||||
...this.sessionDetailsById,
|
||||
[session.id]: detail
|
||||
[session.id]: detail,
|
||||
};
|
||||
this.loadingDetailSessionIds.delete(session.id);
|
||||
},
|
||||
error: (err) => {
|
||||
this.loadingDetailSessionIds.delete(session.id);
|
||||
this.errorMessage = this.extractErrorMessage(err, 'Impossibile caricare il dettaglio sessione.');
|
||||
}
|
||||
this.errorMessage = this.extractErrorMessage(
|
||||
err,
|
||||
'Impossibile caricare il dettaglio sessione.',
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,12 @@
|
||||
<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="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>
|
||||
|
||||
@@ -54,7 +54,10 @@
|
||||
font-weight: 600;
|
||||
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;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
background-color 0.2s ease,
|
||||
color 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -78,7 +81,9 @@
|
||||
padding: var(--space-3) var(--space-4);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease, background-color 0.2s ease;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.logout:hover {
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { ActivatedRoute, Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
RouterLink,
|
||||
RouterLinkActive,
|
||||
RouterOutlet,
|
||||
} from '@angular/router';
|
||||
import { AdminAuthService } from '../services/admin-auth.service';
|
||||
|
||||
const SUPPORTED_LANGS = new Set(['it', 'en', 'de', 'fr']);
|
||||
@@ -10,7 +16,7 @@ const SUPPORTED_LANGS = new Set(['it', 'en', 'de', 'fr']);
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterOutlet, RouterLink, RouterLinkActive],
|
||||
templateUrl: './admin-shell.component.html',
|
||||
styleUrl: './admin-shell.component.scss'
|
||||
styleUrl: './admin-shell.component.scss',
|
||||
})
|
||||
export class AdminShellComponent {
|
||||
private readonly adminAuthService = inject(AdminAuthService);
|
||||
@@ -24,7 +30,7 @@ export class AdminShellComponent {
|
||||
},
|
||||
error: () => {
|
||||
void this.router.navigate(['/', this.resolveLang(), 'admin', 'login']);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user