style: apply prettier formatting

This commit is contained in:
printcalc-ci
2026-03-03 11:46:26 +00:00
parent dd6f723271
commit 20293cc044
131 changed files with 5674 additions and 3482 deletions

View File

@@ -4,34 +4,52 @@ import { adminAuthGuard } from './guards/admin-auth.guard';
export const ADMIN_ROUTES: Routes = [
{
path: 'login',
loadComponent: () => import('./pages/admin-login.component').then(m => m.AdminLoginComponent)
loadComponent: () =>
import('./pages/admin-login.component').then(
(m) => m.AdminLoginComponent,
),
},
{
path: '',
canActivate: [adminAuthGuard],
loadComponent: () => import('./pages/admin-shell.component').then(m => m.AdminShellComponent),
loadComponent: () =>
import('./pages/admin-shell.component').then(
(m) => m.AdminShellComponent,
),
children: [
{
path: '',
pathMatch: 'full',
redirectTo: 'orders'
redirectTo: 'orders',
},
{
path: 'orders',
loadComponent: () => import('./pages/admin-dashboard.component').then(m => m.AdminDashboardComponent)
loadComponent: () =>
import('./pages/admin-dashboard.component').then(
(m) => m.AdminDashboardComponent,
),
},
{
path: 'filament-stock',
loadComponent: () => import('./pages/admin-filament-stock.component').then(m => m.AdminFilamentStockComponent)
loadComponent: () =>
import('./pages/admin-filament-stock.component').then(
(m) => m.AdminFilamentStockComponent,
),
},
{
path: 'contact-requests',
loadComponent: () => import('./pages/admin-contact-requests.component').then(m => m.AdminContactRequestsComponent)
loadComponent: () =>
import('./pages/admin-contact-requests.component').then(
(m) => m.AdminContactRequestsComponent,
),
},
{
path: 'sessions',
loadComponent: () => import('./pages/admin-sessions.component').then(m => m.AdminSessionsComponent)
}
]
}
loadComponent: () =>
import('./pages/admin-sessions.component').then(
(m) => m.AdminSessionsComponent,
),
},
],
},
];

View File

@@ -1,5 +1,11 @@
import { inject } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
import {
ActivatedRouteSnapshot,
CanActivateFn,
Router,
RouterStateSnapshot,
UrlTree,
} from '@angular/router';
import { catchError, map, Observable, of } from 'rxjs';
import { AdminAuthService } from '../services/admin-auth.service';
@@ -17,7 +23,7 @@ function resolveLang(route: ActivatedRouteSnapshot): string {
export const adminAuthGuard: CanActivateFn = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
state: RouterStateSnapshot,
): Observable<boolean | UrlTree> => {
const authService = inject(AdminAuthService);
const router = inject(Router);
@@ -29,13 +35,15 @@ export const adminAuthGuard: CanActivateFn = (
return true;
}
return router.createUrlTree(['/', lang, 'admin', 'login'], {
queryParams: { redirect: state.url }
queryParams: { redirect: state.url },
});
}),
catchError(() => of(
router.createUrlTree(['/', lang, 'admin', 'login'], {
queryParams: { redirect: state.url }
})
))
catchError(() =>
of(
router.createUrlTree(['/', lang, 'admin', 'login'], {
queryParams: { redirect: state.url },
}),
),
),
);
};

View File

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

View File

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

View File

@@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 {

View File

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

View File

@@ -11,23 +11,31 @@ export interface AdminAuthResponse {
}
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class AdminAuthService {
private readonly http = inject(HttpClient);
private readonly baseUrl = `${environment.apiUrl}/api/admin/auth`;
login(password: string): Observable<AdminAuthResponse> {
return this.http.post<AdminAuthResponse>(`${this.baseUrl}/login`, { password }, { withCredentials: true });
return this.http.post<AdminAuthResponse>(
`${this.baseUrl}/login`,
{ password },
{ withCredentials: true },
);
}
logout(): Observable<void> {
return this.http.post<void>(`${this.baseUrl}/logout`, {}, { withCredentials: true });
return this.http.post<void>(
`${this.baseUrl}/logout`,
{},
{ withCredentials: true },
);
}
me(): Observable<boolean> {
return this.http.get<AdminAuthResponse>(`${this.baseUrl}/me`, { withCredentials: true }).pipe(
map((response) => Boolean(response?.authenticated))
);
return this.http
.get<AdminAuthResponse>(`${this.baseUrl}/me`, { withCredentials: true })
.pipe(map((response) => Boolean(response?.authenticated)));
}
}

View File

@@ -145,82 +145,138 @@ export interface AdminQuoteSessionDetail {
}
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class AdminOperationsService {
private readonly http = inject(HttpClient);
private readonly baseUrl = `${environment.apiUrl}/api/admin`;
getFilamentStock(): Observable<AdminFilamentStockRow[]> {
return this.http.get<AdminFilamentStockRow[]>(`${this.baseUrl}/filament-stock`, { withCredentials: true });
return this.http.get<AdminFilamentStockRow[]>(
`${this.baseUrl}/filament-stock`,
{ withCredentials: true },
);
}
getFilamentMaterials(): Observable<AdminFilamentMaterialType[]> {
return this.http.get<AdminFilamentMaterialType[]>(`${this.baseUrl}/filaments/materials`, { withCredentials: true });
return this.http.get<AdminFilamentMaterialType[]>(
`${this.baseUrl}/filaments/materials`,
{ withCredentials: true },
);
}
getFilamentVariants(): Observable<AdminFilamentVariant[]> {
return this.http.get<AdminFilamentVariant[]>(`${this.baseUrl}/filaments/variants`, { withCredentials: true });
return this.http.get<AdminFilamentVariant[]>(
`${this.baseUrl}/filaments/variants`,
{ withCredentials: true },
);
}
createFilamentMaterial(payload: AdminUpsertFilamentMaterialTypePayload): Observable<AdminFilamentMaterialType> {
return this.http.post<AdminFilamentMaterialType>(`${this.baseUrl}/filaments/materials`, payload, { withCredentials: true });
createFilamentMaterial(
payload: AdminUpsertFilamentMaterialTypePayload,
): Observable<AdminFilamentMaterialType> {
return this.http.post<AdminFilamentMaterialType>(
`${this.baseUrl}/filaments/materials`,
payload,
{ withCredentials: true },
);
}
updateFilamentMaterial(materialId: number, payload: AdminUpsertFilamentMaterialTypePayload): Observable<AdminFilamentMaterialType> {
return this.http.put<AdminFilamentMaterialType>(`${this.baseUrl}/filaments/materials/${materialId}`, payload, { withCredentials: true });
updateFilamentMaterial(
materialId: number,
payload: AdminUpsertFilamentMaterialTypePayload,
): Observable<AdminFilamentMaterialType> {
return this.http.put<AdminFilamentMaterialType>(
`${this.baseUrl}/filaments/materials/${materialId}`,
payload,
{ withCredentials: true },
);
}
createFilamentVariant(payload: AdminUpsertFilamentVariantPayload): Observable<AdminFilamentVariant> {
return this.http.post<AdminFilamentVariant>(`${this.baseUrl}/filaments/variants`, payload, { withCredentials: true });
createFilamentVariant(
payload: AdminUpsertFilamentVariantPayload,
): Observable<AdminFilamentVariant> {
return this.http.post<AdminFilamentVariant>(
`${this.baseUrl}/filaments/variants`,
payload,
{ withCredentials: true },
);
}
updateFilamentVariant(variantId: number, payload: AdminUpsertFilamentVariantPayload): Observable<AdminFilamentVariant> {
return this.http.put<AdminFilamentVariant>(`${this.baseUrl}/filaments/variants/${variantId}`, payload, { withCredentials: true });
updateFilamentVariant(
variantId: number,
payload: AdminUpsertFilamentVariantPayload,
): Observable<AdminFilamentVariant> {
return this.http.put<AdminFilamentVariant>(
`${this.baseUrl}/filaments/variants/${variantId}`,
payload,
{ withCredentials: true },
);
}
deleteFilamentVariant(variantId: number): Observable<void> {
return this.http.delete<void>(`${this.baseUrl}/filaments/variants/${variantId}`, { withCredentials: true });
return this.http.delete<void>(
`${this.baseUrl}/filaments/variants/${variantId}`,
{ withCredentials: true },
);
}
getContactRequests(): Observable<AdminContactRequest[]> {
return this.http.get<AdminContactRequest[]>(`${this.baseUrl}/contact-requests`, { withCredentials: true });
return this.http.get<AdminContactRequest[]>(
`${this.baseUrl}/contact-requests`,
{ withCredentials: true },
);
}
getContactRequestDetail(requestId: string): Observable<AdminContactRequestDetail> {
return this.http.get<AdminContactRequestDetail>(`${this.baseUrl}/contact-requests/${requestId}`, { withCredentials: true });
getContactRequestDetail(
requestId: string,
): Observable<AdminContactRequestDetail> {
return this.http.get<AdminContactRequestDetail>(
`${this.baseUrl}/contact-requests/${requestId}`,
{ withCredentials: true },
);
}
updateContactRequestStatus(
requestId: string,
payload: AdminUpdateContactRequestStatusPayload
payload: AdminUpdateContactRequestStatusPayload,
): Observable<AdminContactRequestDetail> {
return this.http.patch<AdminContactRequestDetail>(
`${this.baseUrl}/contact-requests/${requestId}/status`,
payload,
{ withCredentials: true }
{ withCredentials: true },
);
}
downloadContactRequestAttachment(requestId: string, attachmentId: string): Observable<Blob> {
return this.http.get(`${this.baseUrl}/contact-requests/${requestId}/attachments/${attachmentId}/file`, {
withCredentials: true,
responseType: 'blob'
});
downloadContactRequestAttachment(
requestId: string,
attachmentId: string,
): Observable<Blob> {
return this.http.get(
`${this.baseUrl}/contact-requests/${requestId}/attachments/${attachmentId}/file`,
{
withCredentials: true,
responseType: 'blob',
},
);
}
getSessions(): Observable<AdminQuoteSession[]> {
return this.http.get<AdminQuoteSession[]>(`${this.baseUrl}/sessions`, { withCredentials: true });
return this.http.get<AdminQuoteSession[]>(`${this.baseUrl}/sessions`, {
withCredentials: true,
});
}
deleteSession(sessionId: string): Observable<void> {
return this.http.delete<void>(`${this.baseUrl}/sessions/${sessionId}`, { withCredentials: true });
return this.http.delete<void>(`${this.baseUrl}/sessions/${sessionId}`, {
withCredentials: true,
});
}
getSessionDetail(sessionId: string): Observable<AdminQuoteSessionDetail> {
return this.http.get<AdminQuoteSessionDetail>(
`${environment.apiUrl}/api/quote-sessions/${sessionId}`,
{ withCredentials: true }
{ withCredentials: true },
);
}
}

View File

@@ -38,7 +38,7 @@ export interface AdminUpdateOrderStatusPayload {
}
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class AdminOrdersService {
private readonly http = inject(HttpClient);
@@ -49,35 +49,54 @@ export class AdminOrdersService {
}
getOrder(orderId: string): Observable<AdminOrder> {
return this.http.get<AdminOrder>(`${this.baseUrl}/${orderId}`, { withCredentials: true });
return this.http.get<AdminOrder>(`${this.baseUrl}/${orderId}`, {
withCredentials: true,
});
}
confirmPayment(orderId: string, method: string): Observable<AdminOrder> {
return this.http.post<AdminOrder>(`${this.baseUrl}/${orderId}/payments/confirm`, { method }, { withCredentials: true });
return this.http.post<AdminOrder>(
`${this.baseUrl}/${orderId}/payments/confirm`,
{ method },
{ withCredentials: true },
);
}
updateOrderStatus(orderId: string, payload: AdminUpdateOrderStatusPayload): Observable<AdminOrder> {
return this.http.post<AdminOrder>(`${this.baseUrl}/${orderId}/status`, payload, { withCredentials: true });
updateOrderStatus(
orderId: string,
payload: AdminUpdateOrderStatusPayload,
): Observable<AdminOrder> {
return this.http.post<AdminOrder>(
`${this.baseUrl}/${orderId}/status`,
payload,
{ withCredentials: true },
);
}
downloadOrderItemFile(orderId: string, orderItemId: string): Observable<Blob> {
return this.http.get(`${this.baseUrl}/${orderId}/items/${orderItemId}/file`, {
withCredentials: true,
responseType: 'blob'
});
downloadOrderItemFile(
orderId: string,
orderItemId: string,
): Observable<Blob> {
return this.http.get(
`${this.baseUrl}/${orderId}/items/${orderItemId}/file`,
{
withCredentials: true,
responseType: 'blob',
},
);
}
downloadOrderConfirmation(orderId: string): Observable<Blob> {
return this.http.get(`${this.baseUrl}/${orderId}/documents/confirmation`, {
withCredentials: true,
responseType: 'blob'
responseType: 'blob',
});
}
downloadOrderInvoice(orderId: string): Observable<Blob> {
return this.http.get(`${this.baseUrl}/${orderId}/documents/invoice`, {
withCredentials: true,
responseType: 'blob'
responseType: 'blob',
});
}
}