dev #8

Closed
JoeKung wants to merge 72 commits from dev into int
10 changed files with 160 additions and 16 deletions
Showing only changes of commit 521009de7c - Show all commits

View File

@@ -0,0 +1,61 @@
package com.printcalculator.service;
import com.printcalculator.model.ModelDimensions;
import org.junit.jupiter.api.Test;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
class SlicerServiceTest {
@Test
void parseModelDimensionsFromInfoOutput_validOutput_returnsDimensions() {
String output = """
[file.stl]
size_x = 130.860428
size_y = 225.000000
size_z = 140.000000
min_x = 0.000000
""";
Optional<ModelDimensions> dimensions = SlicerService.parseModelDimensionsFromInfoOutput(output);
assertTrue(dimensions.isPresent());
assertEquals(130.860428, dimensions.get().xMm(), 0.000001);
assertEquals(225.0, dimensions.get().yMm(), 0.000001);
assertEquals(140.0, dimensions.get().zMm(), 0.000001);
}
@Test
void parseModelDimensionsFromInfoOutput_withNoise_returnsDimensions() {
String output = """
[2026-02-27 10:26:30.306251] [0x1] [trace] Initializing StaticPrintConfigs
[model.3mf]
size_x = 97.909241
size_y = 97.909241
size_z = 70.000008
[2026-02-27 10:26:30.314575] [0x1] [error] calc_exclude_triangles
""";
Optional<ModelDimensions> dimensions = SlicerService.parseModelDimensionsFromInfoOutput(output);
assertTrue(dimensions.isPresent());
assertEquals(97.909241, dimensions.get().xMm(), 0.000001);
assertEquals(97.909241, dimensions.get().yMm(), 0.000001);
assertEquals(70.000008, dimensions.get().zMm(), 0.000001);
}
@Test
void parseModelDimensionsFromInfoOutput_missingValues_returnsEmpty() {
String output = """
[model.step]
size_x = 10.0
size_y = 20.0
""";
Optional<ModelDimensions> dimensions = SlicerService.parseModelDimensionsFromInfoOutput(output);
assertTrue(dimensions.isEmpty());
}
}

View File

@@ -51,12 +51,21 @@
<input
type="number"
min="1"
[max]="maxInputQuantity"
[ngModel]="item.quantity"
(ngModelChange)="updateQuantity(i, $event)"
class="qty-input">
</div>
<div class="item-price">
{{ (item.unitPrice * item.quantity) | currency:result().currency }}
<span class="item-total-price">
{{ (item.unitPrice * item.quantity) | currency:result().currency }}
</span>
<small class="item-unit-price" *ngIf="item.quantity > 1; else unitPricePlaceholder">
{{ item.unitPrice | currency:result().currency }} {{ 'CHECKOUT.PER_PIECE' | translate }}
</small>
<ng-template #unitPricePlaceholder>
<small class="item-unit-price item-unit-price--placeholder">&nbsp;</small>
</ng-template>
</div>
</div>
</div>
@@ -67,9 +76,13 @@
<app-button variant="outline" (click)="consult.emit()">
{{ 'QUOTE.CONSULT' | translate }}
</app-button>
<app-button (click)="proceed.emit()">
{{ 'QUOTE.PROCEED_ORDER' | translate }}
</app-button>
@if (!hasQuantityOverLimit()) {
<app-button (click)="proceed.emit()">
{{ 'QUOTE.PROCEED_ORDER' | translate }}
</app-button>
} @else {
<small class="limit-note">{{ 'QUOTE.MAX_QTY_NOTICE' | translate:{ max: directOrderLimit } }}</small>
}
</div>
</app-card>

View File

@@ -60,6 +60,27 @@
font-weight: 600;
min-width: 60px;
text-align: right;
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: center;
min-height: 2.1rem;
}
.item-total-price {
line-height: 1.1;
}
.item-unit-price {
margin-top: 2px;
font-size: 0.72rem;
font-weight: 400;
color: var(--color-text-muted);
line-height: 1.2;
}
.item-unit-price--placeholder {
visibility: hidden;
}
.result-grid {
@@ -84,6 +105,13 @@
.actions { display: flex; flex-direction: column; gap: var(--space-3); }
.limit-note {
font-size: 0.8rem;
color: var(--color-text-muted);
text-align: center;
margin-top: calc(var(--space-2) * -1);
}
.notes-section {
margin-top: var(--space-4);
margin-bottom: var(--space-4);

View File

@@ -15,6 +15,9 @@ import { QuoteResult, QuoteItem } from '../../services/quote-estimator.service';
styleUrl: './quote-result.component.scss'
})
export class QuoteResultComponent {
readonly maxInputQuantity = 500;
readonly directOrderLimit = 100;
result = input.required<QuoteResult>();
consult = output<void>();
proceed = output<void>();
@@ -34,20 +37,23 @@ export class QuoteResultComponent {
updateQuantity(index: number, newQty: number | string) {
const qty = typeof newQty === 'string' ? parseInt(newQty, 10) : newQty;
if (qty < 1 || isNaN(qty)) return;
const normalizedQty = Math.min(qty, this.maxInputQuantity);
this.items.update(current => {
const updated = [...current];
updated[index] = { ...updated[index], quantity: qty };
updated[index] = { ...updated[index], quantity: normalizedQty };
return updated;
});
this.itemChange.emit({
id: this.items()[index].id,
fileName: this.items()[index].fileName,
quantity: qty
quantity: normalizedQty
});
}
hasQuantityOverLimit = computed(() => this.items().some(item => item.quantity > this.directOrderLimit));
totals = computed(() => {
const currentItems = this.items();
const setup = this.result().setupCost;

View File

@@ -122,10 +122,15 @@
<div class="summary-item" *ngFor="let item of quote()!.items">
<div class="item-info">
<span class="item-name">{{ item.fileName }}</span>
<span class="item-meta">{{ item.material }} - {{ item.color || 'Default' }}</span>
<span class="item-meta">{{ item.material }} - {{ item.color || ('USER_DETAILS.DEFAULT_COLOR' | translate) }}</span>
</div>
<div class="item-qty">x{{ item.quantity }}</div>
<div class="item-price">{{ (item.unitPrice * item.quantity) | currency:'CHF' }}</div>
<div class="item-price">
<span class="item-total-price">{{ (item.unitPrice * item.quantity) | currency:'CHF' }}</span>
<small class="item-unit-price" *ngIf="item.quantity > 1">
{{ item.unitPrice | currency:'CHF' }} {{ 'CHECKOUT.PER_PIECE' | translate }}
</small>
</div>
</div>
<hr>

View File

@@ -112,6 +112,17 @@
.item-price {
font-weight: 600;
display: flex;
flex-direction: column;
align-items: flex-end;
}
.item-unit-price {
margin-top: 2px;
font-size: 0.72rem;
font-weight: 400;
color: var(--color-text-muted);
line-height: 1.2;
}
.total-row {

View File

@@ -10,7 +10,7 @@
<div class="checkout-form-section">
<!-- Error Message -->
<div *ngIf="error" class="error-message">
{{ error }}
{{ error | translate }}
</div>
<form [formGroup]="checkoutForm" (ngSubmit)="onSubmit()" *ngIf="!error">
@@ -142,7 +142,12 @@
</div>
</div>
<div class="item-price">
{{ (item.unitPriceChf * item.quantity) | currency:'CHF' }}
<span class="item-total-price">
{{ (item.unitPriceChf * item.quantity) | currency:'CHF' }}
</span>
<small class="item-unit-price" *ngIf="item.quantity > 1">
{{ item.unitPriceChf | currency:'CHF' }} {{ 'CHECKOUT.PER_PIECE' | translate }}
</small>
</div>
</div>
</div>

View File

@@ -240,6 +240,17 @@ app-toggle-selector.user-type-selector-compact {
margin-left: var(--space-3);
white-space: nowrap;
color: var(--color-text);
display: flex;
flex-direction: column;
align-items: flex-end;
.item-unit-price {
margin-top: 2px;
font-size: 0.75rem;
font-weight: 400;
color: var(--color-text-muted);
line-height: 1.2;
}
}
}

View File

@@ -7,7 +7,8 @@
"QUOTE": {
"CONSULT": "Request Consultation",
"PROCEED_ORDER": "Proceed to Order",
"TOTAL": "Total Estimate"
"TOTAL": "Total Estimate",
"MAX_QTY_NOTICE": "For quantities above {{max}} pieces, request consultation."
},
"USER_DETAILS": {
"TITLE": "Shipping Details",
@@ -208,6 +209,7 @@
"SETUP_FEE": "Setup Fee",
"TOTAL": "Total",
"QTY": "Qty",
"PER_PIECE": "per piece",
"SHIPPING": "Shipping"
},
"PAYMENT": {

View File

@@ -53,7 +53,7 @@
"CARD_SHOP_3_TITLE": "Su richiesta",
"CARD_SHOP_3_TEXT": "Non trovi quello che serve? Lo progettiamo e lo produciamo per te.",
"SEC_ABOUT_TITLE": "Su di noi",
"SEC_ABOUT_TEXT": "Siamo due studenti di ingegneria: la stampa 3D ci ha conquistati per un motivo semplice, vedere un problema e costruire la soluzione. Da questa idea prendono forma prototipi, componenti oggetti pensati per funzionare nella quotidianità. ",
"SEC_ABOUT_TEXT": "Siamo due studenti di ingegneria: la stampa 3D ci ha conquistati per un motivo semplice, vedere un problema e costruire la soluzione. Da questa idea prendono forma prototipi, oggetti pensati per funzionare nella quotidianità. ",
"FOUNDERS_PHOTO": "Foto Founders"
},
"CALC": {
@@ -103,13 +103,14 @@
"PROCESSING": "Elaborazione...",
"NOTES_PLACEHOLDER": "Istruzioni specifiche...",
"SETUP_NOTE": "* Include {{cost}} Costo di Setup",
"SHIPPING_NOTE": "** costi di spedizione esclusi calcolati al prossimo passaggio",
"SHIPPING_NOTE": "** costi di spedizione esclusi, calcolati al prossimo passaggio",
"STEP_WARNING": "La visualizzazione 3D non è compatibile con i file step e 3mf"
},
"QUOTE": {
"PROCEED_ORDER": "Procedi con l'ordine",
"CONSULT": "Richiedi Consulenza",
"TOTAL": "Totale"
"TOTAL": "Totale",
"MAX_QTY_NOTICE": "Per quantità oltre {{max}} pezzi, richiedi consulenza."
},
"USER_DETAILS": {
"TITLE": "I tuoi dati",
@@ -153,7 +154,7 @@
"TITLE": "Chi Siamo",
"EYEBROW": "Laboratorio di stampa 3D",
"SUBTITLE": "Siamo due studenti con tanta voglia di fare e di imparare.",
"HOW_TEXT": "3D Fab nasce dallinteresse iniziale di Matteo per la stampa 3D. Ha comprato una stampante e ha iniziato a sperimentare sul serio. \n A un certo punto sono arrivate le prime richieste: un pezzo rotto da sostituire, un ricambio che non si trova, un adattatore comodo da avere. Le richieste sono aumentate e ci siamo detti: ok, facciamolo bene.\nOggi trasformiamo bisogni concreti e idee in pezzi stampati, con costi trasparenti.\nIn seguito abbiamo creato un calcolatore per capire il costo in anticipo: è stato uno dei primi passi che ci ha fatto passare dal “facciamo qualche pezzo” a un progetto vero, insieme.",
"HOW_TEXT": "3D Fab nasce dallinteresse iniziale di Matteo per la stampa 3D. Ha comprato una stampante e ha iniziato a sperimentare sul serio. \n A un certo punto sono arrivate le prime richieste: un pezzo rotto da sostituire, un ricambio che non si trova, un adattatore comodo da avere. Le richieste sono aumentate e ci siamo detti: ok, facciamolo bene.\nIn seguito abbiamo creato un calcolatore per capire il costo in anticipo: è stato uno dei primi passi che ci ha fatto passare dal “facciamo qualche pezzo” a un progetto vero, insieme.",
"PASSIONS_TITLE": "Le nostre passioni",
"PASSION_BIKE_TRIAL": "Bike trial",
"PASSION_MOUNTAIN": "Montagna",
@@ -411,6 +412,7 @@
"SETUP_FEE": "Costo di Avvio",
"TOTAL": "Totale",
"QTY": "Qtà",
"PER_PIECE": "al pezzo",
"SHIPPING": "Spedizione (CH)",
"INVALID_EMAIL": "Email non valida",
"COMPANY_OPTIONAL": "Nome Azienda (Opzionale)",