From 521009de7cf0398ee66b8ebd533be9992f93e407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Fri, 27 Feb 2026 10:59:02 +0100 Subject: [PATCH] =?UTF-8?q?feat(front-end):=20gestione=20quantit=C3=A0=20m?= =?UTF-8?q?assima=20e=20price=20for=20piece?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/SlicerServiceTest.java | 61 +++++++++++++++++++ .../quote-result/quote-result.component.html | 23 +++++-- .../quote-result/quote-result.component.scss | 28 +++++++++ .../quote-result/quote-result.component.ts | 10 ++- .../user-details/user-details.component.html | 9 ++- .../user-details/user-details.component.scss | 11 ++++ .../features/checkout/checkout.component.html | 9 ++- .../features/checkout/checkout.component.scss | 11 ++++ frontend/src/assets/i18n/en.json | 4 +- frontend/src/assets/i18n/it.json | 10 +-- 10 files changed, 160 insertions(+), 16 deletions(-) create mode 100644 backend/src/test/java/com/printcalculator/service/SlicerServiceTest.java diff --git a/backend/src/test/java/com/printcalculator/service/SlicerServiceTest.java b/backend/src/test/java/com/printcalculator/service/SlicerServiceTest.java new file mode 100644 index 0000000..f6872aa --- /dev/null +++ b/backend/src/test/java/com/printcalculator/service/SlicerServiceTest.java @@ -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 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 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 dimensions = SlicerService.parseModelDimensionsFromInfoOutput(output); + + assertTrue(dimensions.isEmpty()); + } +} diff --git a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html index 6f06312..fd42252 100644 --- a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html +++ b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html @@ -51,12 +51,21 @@
- {{ (item.unitPrice * item.quantity) | currency:result().currency }} + + {{ (item.unitPrice * item.quantity) | currency:result().currency }} + + + {{ item.unitPrice | currency:result().currency }} {{ 'CHECKOUT.PER_PIECE' | translate }} + + +   +
@@ -67,9 +76,13 @@ {{ 'QUOTE.CONSULT' | translate }} - - - {{ 'QUOTE.PROCEED_ORDER' | translate }} - + + @if (!hasQuantityOverLimit()) { + + {{ 'QUOTE.PROCEED_ORDER' | translate }} + + } @else { + {{ 'QUOTE.MAX_QTY_NOTICE' | translate:{ max: directOrderLimit } }} + } diff --git a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.scss b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.scss index c9f6973..effe200 100644 --- a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.scss +++ b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.scss @@ -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); diff --git a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts index 8d3e193..dfe8138 100644 --- a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts +++ b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts @@ -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(); consult = output(); proceed = output(); @@ -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; diff --git a/frontend/src/app/features/calculator/components/user-details/user-details.component.html b/frontend/src/app/features/calculator/components/user-details/user-details.component.html index 0e4a4c0..f6b282f 100644 --- a/frontend/src/app/features/calculator/components/user-details/user-details.component.html +++ b/frontend/src/app/features/calculator/components/user-details/user-details.component.html @@ -122,10 +122,15 @@
{{ item.fileName }} - {{ item.material }} - {{ item.color || 'Default' }} + {{ item.material }} - {{ item.color || ('USER_DETAILS.DEFAULT_COLOR' | translate) }}
x{{ item.quantity }}
-
{{ (item.unitPrice * item.quantity) | currency:'CHF' }}
+
+ {{ (item.unitPrice * item.quantity) | currency:'CHF' }} + + {{ item.unitPrice | currency:'CHF' }} {{ 'CHECKOUT.PER_PIECE' | translate }} + +

diff --git a/frontend/src/app/features/calculator/components/user-details/user-details.component.scss b/frontend/src/app/features/calculator/components/user-details/user-details.component.scss index 9d29832..16dd410 100644 --- a/frontend/src/app/features/calculator/components/user-details/user-details.component.scss +++ b/frontend/src/app/features/calculator/components/user-details/user-details.component.scss @@ -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 { diff --git a/frontend/src/app/features/checkout/checkout.component.html b/frontend/src/app/features/checkout/checkout.component.html index 9cfa854..b335cf9 100644 --- a/frontend/src/app/features/checkout/checkout.component.html +++ b/frontend/src/app/features/checkout/checkout.component.html @@ -10,7 +10,7 @@
- {{ error }} + {{ error | translate }}
@@ -142,7 +142,12 @@
- {{ (item.unitPriceChf * item.quantity) | currency:'CHF' }} + + {{ (item.unitPriceChf * item.quantity) | currency:'CHF' }} + + + {{ item.unitPriceChf | currency:'CHF' }} {{ 'CHECKOUT.PER_PIECE' | translate }} +
diff --git a/frontend/src/app/features/checkout/checkout.component.scss b/frontend/src/app/features/checkout/checkout.component.scss index 271151b..d8383a9 100644 --- a/frontend/src/app/features/checkout/checkout.component.scss +++ b/frontend/src/app/features/checkout/checkout.component.scss @@ -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; + } } } diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json index f71eb0b..79c5ea3 100644 --- a/frontend/src/assets/i18n/en.json +++ b/frontend/src/assets/i18n/en.json @@ -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": { diff --git a/frontend/src/assets/i18n/it.json b/frontend/src/assets/i18n/it.json index 16c5740..562fe3c 100644 --- a/frontend/src/assets/i18n/it.json +++ b/frontend/src/assets/i18n/it.json @@ -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 dall’interesse 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 dall’interesse 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)",