feat(back-end): shop ui implementation
This commit is contained in:
@@ -67,12 +67,26 @@
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="toolbar-field" for="order-type-filter">
|
||||
<span>Tipo ordine</span>
|
||||
<select
|
||||
class="ui-form-control"
|
||||
id="order-type-filter"
|
||||
[ngModel]="orderTypeFilter"
|
||||
(ngModelChange)="onOrderTypeFilterChange($event)"
|
||||
>
|
||||
<option *ngFor="let option of orderTypeFilterOptions" [ngValue]="option">
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="table-wrap ui-table-wrap">
|
||||
<table class="ui-data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Ordine</th>
|
||||
<th>Tipo</th>
|
||||
<th>Email</th>
|
||||
<th>Pagamento</th>
|
||||
<th>Stato ordine</th>
|
||||
@@ -86,6 +100,15 @@
|
||||
(click)="openDetails(order.id)"
|
||||
>
|
||||
<td>{{ order.orderNumber }}</td>
|
||||
<td>
|
||||
<span
|
||||
class="order-type-badge"
|
||||
[class.order-type-badge--shop]="orderKind(order) === 'SHOP'"
|
||||
[class.order-type-badge--mixed]="orderKind(order) === 'MIXED'"
|
||||
>
|
||||
{{ orderKindLabel(order) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ order.customerEmail }}</td>
|
||||
<td>{{ order.paymentStatus || "PENDING" }}</td>
|
||||
<td>{{ order.status }}</td>
|
||||
@@ -94,7 +117,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="no-results" *ngIf="filteredOrders.length === 0">
|
||||
<td colspan="5">
|
||||
<td colspan="6">
|
||||
Nessun ordine trovato per i filtri selezionati.
|
||||
</td>
|
||||
</tr>
|
||||
@@ -105,7 +128,16 @@
|
||||
|
||||
<section class="detail-panel ui-detail-panel" *ngIf="selectedOrder">
|
||||
<div class="detail-header">
|
||||
<h2>Dettaglio ordine {{ selectedOrder.orderNumber }}</h2>
|
||||
<div class="detail-title-row">
|
||||
<h2>Dettaglio ordine {{ selectedOrder.orderNumber }}</h2>
|
||||
<span
|
||||
class="order-type-badge"
|
||||
[class.order-type-badge--shop]="orderKind(selectedOrder) === 'SHOP'"
|
||||
[class.order-type-badge--mixed]="orderKind(selectedOrder) === 'MIXED'"
|
||||
>
|
||||
{{ orderKindLabel(selectedOrder) }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="order-uuid">
|
||||
UUID:
|
||||
<code
|
||||
@@ -129,6 +161,9 @@
|
||||
<div class="ui-meta-item">
|
||||
<strong>Stato ordine</strong><span>{{ selectedOrder.status }}</span>
|
||||
</div>
|
||||
<div class="ui-meta-item">
|
||||
<strong>Tipo ordine</strong><span>{{ orderKindLabel(selectedOrder) }}</span>
|
||||
</div>
|
||||
<div class="ui-meta-item">
|
||||
<strong>Totale</strong
|
||||
><span>{{
|
||||
@@ -207,6 +242,7 @@
|
||||
type="button"
|
||||
class="ui-button ui-button--ghost"
|
||||
(click)="openPrintDetails()"
|
||||
[disabled]="!hasPrintItems(selectedOrder)"
|
||||
>
|
||||
Dettagli stampa
|
||||
</button>
|
||||
@@ -215,38 +251,60 @@
|
||||
<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="item-meta">
|
||||
Qta: {{ item.quantity }} | Materiale:
|
||||
{{ getItemMaterialLabel(item) }} | Colore:
|
||||
<div class="item-heading">
|
||||
<p class="file-name">
|
||||
<strong>{{ itemDisplayName(item) }}</strong>
|
||||
</p>
|
||||
<span
|
||||
class="color-swatch"
|
||||
*ngIf="getItemColorHex(item) as colorHex"
|
||||
[style.background-color]="colorHex"
|
||||
></span>
|
||||
<span>
|
||||
{{ getItemColorLabel(item) }}
|
||||
<ng-container *ngIf="getItemColorCodeSuffix(item) as colorCode">
|
||||
({{ colorCode }})
|
||||
</ng-container>
|
||||
class="item-kind-badge"
|
||||
[class.item-kind-badge--shop]="isShopItem(item)"
|
||||
>
|
||||
{{ isShopItem(item) ? "Shop" : "Calcolatore" }}
|
||||
</span>
|
||||
| Nozzle: {{ item.nozzleDiameterMm ?? "-" }} mm | Layer:
|
||||
</div>
|
||||
<p class="item-meta">
|
||||
<span>Qta: {{ item.quantity }}</span>
|
||||
<span *ngIf="showItemMaterial(item)">
|
||||
Materiale: {{ getItemMaterialLabel(item) }}
|
||||
</span>
|
||||
<span *ngIf="itemVariantLabel(item) as variantLabel">
|
||||
Variante: {{ variantLabel }}
|
||||
</span>
|
||||
<span class="item-meta__color">
|
||||
Colore:
|
||||
<span
|
||||
class="color-swatch"
|
||||
*ngIf="getItemColorHex(item) as colorHex"
|
||||
[style.background-color]="colorHex"
|
||||
></span>
|
||||
<span>
|
||||
{{ getItemColorLabel(item) }}
|
||||
<ng-container *ngIf="getItemColorCodeSuffix(item) as colorCode">
|
||||
({{ colorCode }})
|
||||
</ng-container>
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
<p class="item-tech" *ngIf="showItemPrintDetails(item)">
|
||||
Nozzle: {{ item.nozzleDiameterMm ?? "-" }} mm | Layer:
|
||||
{{ item.layerHeightMm ?? "-" }} mm | Infill:
|
||||
{{ item.infillPercent ?? "-" }}% | Supporti:
|
||||
{{ formatSupports(item.supportsEnabled) }}
|
||||
| Riga:
|
||||
</p>
|
||||
<p class="item-total">
|
||||
Riga:
|
||||
{{ item.lineTotalChf | currency: "CHF" : "symbol" : "1.2-2" }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="ui-button ui-button--ghost"
|
||||
(click)="downloadItemFile(item.id, item.originalFilename)"
|
||||
>
|
||||
Scarica file
|
||||
</button>
|
||||
<div class="item-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="ui-button ui-button--ghost"
|
||||
(click)="downloadItemFile(item.id, item.originalFilename || itemDisplayName(item))"
|
||||
>
|
||||
{{ downloadItemLabel(item) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -315,7 +373,7 @@
|
||||
|
||||
<h4>Parametri per file</h4>
|
||||
<div class="file-color-list">
|
||||
<div class="file-color-row" *ngFor="let item of selectedOrder.items">
|
||||
<div class="file-color-row" *ngFor="let item of printItems(selectedOrder)">
|
||||
<span class="filename">{{ item.originalFilename }}</span>
|
||||
<span class="file-color">
|
||||
{{ getItemMaterialLabel(item) }} | Colore:
|
||||
|
||||
@@ -21,10 +21,11 @@
|
||||
|
||||
.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)
|
||||
minmax(170px, 1fr);
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
@@ -69,6 +70,13 @@ tbody tr.no-results:hover {
|
||||
margin: 0 0 var(--space-2);
|
||||
}
|
||||
|
||||
.detail-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.actions-block {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -113,6 +121,15 @@ tbody tr.no-results:hover {
|
||||
|
||||
.item-main {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.item-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.file-name {
|
||||
@@ -124,7 +141,7 @@ tbody tr.no-results:hover {
|
||||
}
|
||||
|
||||
.item-meta {
|
||||
margin: var(--space-1) 0 0;
|
||||
margin: 0;
|
||||
font-size: 0.84rem;
|
||||
color: var(--color-text-muted);
|
||||
display: flex;
|
||||
@@ -133,7 +150,33 @@ tbody tr.no-results:hover {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.item button {
|
||||
.item-meta__color {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.item-tech,
|
||||
.item-total {
|
||||
margin: 0;
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.item-tech {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.item-total {
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.item-actions button {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
@@ -150,6 +193,32 @@ tbody tr.no-results:hover {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.order-type-badge,
|
||||
.item-kind-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-radius: 999px;
|
||||
padding: 0.2rem 0.65rem;
|
||||
background: var(--color-neutral-100);
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.order-type-badge--shop,
|
||||
.item-kind-badge--shop {
|
||||
background: color-mix(in srgb, var(--color-brand) 12%, white);
|
||||
color: var(--color-brand);
|
||||
}
|
||||
|
||||
.order-type-badge--mixed {
|
||||
background: color-mix(in srgb, #f59e0b 16%, white);
|
||||
color: #9a5b00;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -247,6 +316,10 @@ h4 {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.actions-block {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
@@ -26,6 +26,7 @@ export class AdminDashboardComponent implements OnInit {
|
||||
orderSearchTerm = '';
|
||||
paymentStatusFilter = 'ALL';
|
||||
orderStatusFilter = 'ALL';
|
||||
orderTypeFilter = 'ALL';
|
||||
showPrintDetails = false;
|
||||
loading = false;
|
||||
detailLoading = false;
|
||||
@@ -62,6 +63,7 @@ export class AdminDashboardComponent implements OnInit {
|
||||
'COMPLETED',
|
||||
'CANCELLED',
|
||||
];
|
||||
readonly orderTypeFilterOptions = ['ALL', 'SHOP', 'CALCULATOR', 'MIXED'];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadOrders();
|
||||
@@ -117,6 +119,11 @@ export class AdminDashboardComponent implements OnInit {
|
||||
this.applyListFiltersAndSelection();
|
||||
}
|
||||
|
||||
onOrderTypeFilterChange(value: string): void {
|
||||
this.orderTypeFilter = value || 'ALL';
|
||||
this.applyListFiltersAndSelection();
|
||||
}
|
||||
|
||||
openDetails(orderId: string): void {
|
||||
this.detailLoading = true;
|
||||
this.adminOrdersService.getOrder(orderId).subscribe({
|
||||
@@ -124,6 +131,7 @@ export class AdminDashboardComponent implements OnInit {
|
||||
this.selectedOrder = order;
|
||||
this.selectedStatus = order.status;
|
||||
this.selectedPaymentMethod = order.paymentMethod || 'OTHER';
|
||||
this.showPrintDetails = this.showPrintDetails && this.hasPrintItems(order);
|
||||
this.detailLoading = false;
|
||||
},
|
||||
error: () => {
|
||||
@@ -247,6 +255,9 @@ export class AdminDashboardComponent implements OnInit {
|
||||
}
|
||||
|
||||
openPrintDetails(): void {
|
||||
if (!this.selectedOrder || !this.hasPrintItems(this.selectedOrder)) {
|
||||
return;
|
||||
}
|
||||
this.showPrintDetails = true;
|
||||
}
|
||||
|
||||
@@ -267,6 +278,34 @@ export class AdminDashboardComponent implements OnInit {
|
||||
return 'Bozza';
|
||||
}
|
||||
|
||||
isShopItem(item: AdminOrderItem): boolean {
|
||||
return String(item?.itemType ?? '').toUpperCase() === 'SHOP_PRODUCT';
|
||||
}
|
||||
|
||||
itemDisplayName(item: AdminOrderItem): string {
|
||||
const displayName = (item.displayName || '').trim();
|
||||
if (displayName) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
const shopName = (item.shopProductName || '').trim();
|
||||
if (shopName) {
|
||||
return shopName;
|
||||
}
|
||||
|
||||
return (item.originalFilename || '').trim() || '-';
|
||||
}
|
||||
|
||||
itemVariantLabel(item: AdminOrderItem): string | null {
|
||||
const variantLabel = (item.shopVariantLabel || '').trim();
|
||||
if (variantLabel) {
|
||||
return variantLabel;
|
||||
}
|
||||
|
||||
const colorName = (item.shopVariantColorName || '').trim();
|
||||
return colorName || null;
|
||||
}
|
||||
|
||||
isHexColor(value?: string): boolean {
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
@@ -291,12 +330,22 @@ export class AdminDashboardComponent implements OnInit {
|
||||
}
|
||||
|
||||
getItemColorLabel(item: AdminOrderItem): string {
|
||||
const shopColorName = (item.shopVariantColorName || '').trim();
|
||||
if (shopColorName) {
|
||||
return shopColorName;
|
||||
}
|
||||
|
||||
const colorName = (item.filamentColorName || '').trim();
|
||||
const colorCode = (item.colorCode || '').trim();
|
||||
return colorName || colorCode || '-';
|
||||
}
|
||||
|
||||
getItemColorHex(item: AdminOrderItem): string | null {
|
||||
const shopColorHex = (item.shopVariantColorHex || '').trim();
|
||||
if (this.isHexColor(shopColorHex)) {
|
||||
return shopColorHex;
|
||||
}
|
||||
|
||||
const variantHex = (item.filamentColorHex || '').trim();
|
||||
if (this.isHexColor(variantHex)) {
|
||||
return variantHex;
|
||||
@@ -336,6 +385,54 @@ export class AdminDashboardComponent implements OnInit {
|
||||
return 'Supporti -';
|
||||
}
|
||||
|
||||
showItemMaterial(item: AdminOrderItem): boolean {
|
||||
return !this.isShopItem(item);
|
||||
}
|
||||
|
||||
showItemPrintDetails(item: AdminOrderItem): boolean {
|
||||
return !this.isShopItem(item);
|
||||
}
|
||||
|
||||
hasShopItems(order: AdminOrder | null): boolean {
|
||||
return (order?.items || []).some((item) => this.isShopItem(item));
|
||||
}
|
||||
|
||||
hasPrintItems(order: AdminOrder | null): boolean {
|
||||
return (order?.items || []).some((item) => !this.isShopItem(item));
|
||||
}
|
||||
|
||||
printItems(order: AdminOrder | null): AdminOrderItem[] {
|
||||
return (order?.items || []).filter((item) => !this.isShopItem(item));
|
||||
}
|
||||
|
||||
orderKind(order: AdminOrder | null): 'SHOP' | 'CALCULATOR' | 'MIXED' {
|
||||
const hasShop = this.hasShopItems(order);
|
||||
const hasPrint = this.hasPrintItems(order);
|
||||
|
||||
if (hasShop && hasPrint) {
|
||||
return 'MIXED';
|
||||
}
|
||||
if (hasShop) {
|
||||
return 'SHOP';
|
||||
}
|
||||
return 'CALCULATOR';
|
||||
}
|
||||
|
||||
orderKindLabel(order: AdminOrder | null): string {
|
||||
switch (this.orderKind(order)) {
|
||||
case 'SHOP':
|
||||
return 'Shop';
|
||||
case 'MIXED':
|
||||
return 'Misto';
|
||||
default:
|
||||
return 'Calcolatore';
|
||||
}
|
||||
}
|
||||
|
||||
downloadItemLabel(item: AdminOrderItem): string {
|
||||
return this.isShopItem(item) ? 'Scarica modello' : 'Scarica file';
|
||||
}
|
||||
|
||||
isSelected(orderId: string): boolean {
|
||||
return this.selectedOrder?.id === orderId;
|
||||
}
|
||||
@@ -349,6 +446,7 @@ export class AdminDashboardComponent implements OnInit {
|
||||
this.selectedStatus = updatedOrder.status;
|
||||
this.selectedPaymentMethod =
|
||||
updatedOrder.paymentMethod || this.selectedPaymentMethod;
|
||||
this.showPrintDetails = this.showPrintDetails && this.hasPrintItems(updatedOrder);
|
||||
}
|
||||
|
||||
private applyListFiltersAndSelection(): void {
|
||||
@@ -384,8 +482,16 @@ export class AdminDashboardComponent implements OnInit {
|
||||
const matchesOrderStatus =
|
||||
this.orderStatusFilter === 'ALL' ||
|
||||
orderStatus === this.orderStatusFilter;
|
||||
const matchesOrderType =
|
||||
this.orderTypeFilter === 'ALL' ||
|
||||
this.orderKind(order) === this.orderTypeFilter;
|
||||
|
||||
return matchesSearch && matchesPayment && matchesOrderStatus;
|
||||
return (
|
||||
matchesSearch &&
|
||||
matchesPayment &&
|
||||
matchesOrderStatus &&
|
||||
matchesOrderType
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,19 @@ import { environment } from '../../../../environments/environment';
|
||||
|
||||
export interface AdminOrderItem {
|
||||
id: string;
|
||||
itemType: string;
|
||||
originalFilename: string;
|
||||
displayName?: string;
|
||||
materialCode: string;
|
||||
colorCode: string;
|
||||
filamentVariantId?: number;
|
||||
shopProductId?: string;
|
||||
shopProductVariantId?: string;
|
||||
shopProductSlug?: string;
|
||||
shopProductName?: string;
|
||||
shopVariantLabel?: string;
|
||||
shopVariantColorName?: string;
|
||||
shopVariantColorHex?: string;
|
||||
filamentVariantDisplayName?: string;
|
||||
filamentColorName?: string;
|
||||
filamentColorHex?: string;
|
||||
|
||||
Reference in New Issue
Block a user