chore(back-end and front-end): refractor and improvements calculator
All checks were successful
PR Checks / prettier-autofix (pull_request) Successful in 10s
PR Checks / security-sast (pull_request) Successful in 28s
PR Checks / test-backend (pull_request) Successful in 24s
PR Checks / test-frontend (pull_request) Successful in 1m0s

This commit is contained in:
2026-03-05 17:28:07 +01:00
parent 8e23bd97e6
commit a7491130fb
12 changed files with 1989 additions and 1675 deletions

View File

@@ -63,11 +63,11 @@
<span class="file-details">
{{ item.unitTime / 3600 | number: "1.1-1" }}h |
{{ item.unitWeight | number: "1.0-0" }}g |
materiale: {{ item.material || "N/D" }}
@if (getItemDifferenceLabel(item.fileName)) {
<span class="material-chip">{{ item.material || "N/D" }}</span>
@if (getItemDifferenceLabel(item.fileName, item.material)) {
|
<small class="item-settings-diff">
{{ getItemDifferenceLabel(item.fileName) }}
{{ getItemDifferenceLabel(item.fileName, item.material) }}
</small>
}
</span>
@@ -110,7 +110,7 @@
<div class="actions">
<div class="actions-left">
<app-button variant="outline" (click)="consult.emit()">
<app-button variant="secondary" (click)="consult.emit()">
{{ "QUOTE.CONSULT" | translate }}
</app-button>
</div>

View File

@@ -20,10 +20,11 @@
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-3);
background: var(--color-neutral-50);
padding: var(--space-3) var(--space-4);
background: var(--color-bg-card);
border-radius: var(--radius-md);
border: 1px solid var(--color-border);
box-shadow: 0 2px 6px rgba(10, 20, 30, 0.04);
}
.item-info {
@@ -54,6 +55,19 @@
color: var(--color-text-muted);
}
.material-chip {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 999px;
border: 1px solid #d9d4bd;
background: #fbf7e9;
color: #6d5b1d;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.2px;
}
.item-controls {
display: flex;
align-items: center;
@@ -149,6 +163,7 @@
.actions-right {
display: flex;
align-items: center;
gap: var(--space-2);
}
.actions-right {

View File

@@ -189,14 +189,30 @@ export class QuoteResultComponent implements OnDestroy {
this.quantityTimers.clear();
}
getItemDifferenceLabel(fileName: string): string {
getItemDifferenceLabel(fileName: string, materialCode?: string): string {
const differences =
this.itemSettingsDiffByFileName()[fileName]?.differences || [];
if (differences.length === 0) return '';
const materialOnly = differences.find(
const normalizedMaterial = String(materialCode || '')
.trim()
.toLowerCase();
const filtered = differences.filter((entry) => {
const normalized = String(entry || '')
.trim()
.toLowerCase();
const isMaterialOnly = !normalized.includes(':');
return !(isMaterialOnly && normalized === normalizedMaterial);
});
if (filtered.length === 0) {
return '';
}
const materialOnly = filtered.find(
(entry) => !entry.includes(':') && entry.trim().length > 0,
);
return materialOnly || differences.join(' | ');
return materialOnly || filtered.join(' | ');
}
}

View File

@@ -13,11 +13,9 @@
>
</app-stl-viewer>
}
<!-- Close button removed as requested -->
</div>
}
<!-- Initial Dropzone (Visible only when no files) -->
@if (items().length === 0) {
<app-dropzone
[label]="'CALC.UPLOAD_LABEL'"
@@ -29,7 +27,6 @@
</app-dropzone>
}
<!-- New File List with Details -->
@if (items().length > 0) {
<div class="items-grid">
@for (item of items(); track item.file.name; let i = $index) {
@@ -83,7 +80,6 @@
}
</div>
<!-- "Add Files" Button (Visible only when files exist) -->
<div class="add-more-container">
<input
#additionalInput
@@ -111,39 +107,38 @@
>.
</p>
<label class="item-settings-checkbox item-settings-checkbox--top">
<input
type="checkbox"
[checked]="sameSettingsForAll()"
(change)="onSameSettingsToggle($any($event.target).checked)"
/>
<span>Tutti i file uguali (applica impostazioni a tutti)</span>
</label>
@if (mode() === "advanced") {
<div class="sync-settings">
<label class="sync-settings-toggle">
<input
type="checkbox"
[checked]="sameSettingsForAll()"
[disabled]="lockedSettings()"
(change)="onSameSettingsToggle($any($event.target).checked)"
/>
<span class="sync-settings-copy">
<span class="sync-settings-title">
Stesse impostazioni per tutti i file
</span>
<span class="sync-settings-subtitle">Colore escluso</span>
</span>
</label>
</div>
@if (sameSettingsForAll()) {
<div class="item-settings-panel">
<h4 class="item-settings-title">Impostazioni globali</h4>
@if (sameSettingsForAll()) {
<div class="item-settings-panel">
<h4 class="item-settings-title">Impostazioni globali</h4>
<div class="item-settings-grid">
<label>
{{ "CALC.MATERIAL" | translate }}
<select formControlName="material">
@for (mat of materials(); track mat.value) {
<option [value]="mat.value">{{ mat.label }}</option>
}
</select>
</label>
@if (mode() === "easy") {
<div class="item-settings-grid">
<label>
{{ "CALC.QUALITY" | translate }}
<select formControlName="quality">
@for (quality of qualities(); track quality.value) {
<option [value]="quality.value">{{ quality.label }}</option>
{{ "CALC.MATERIAL" | translate }}
<select formControlName="material">
@for (mat of materials(); track mat.value) {
<option [value]="mat.value">{{ mat.label }}</option>
}
</select>
</label>
} @else {
<label>
{{ "CALC.NOZZLE" | translate }}
<select formControlName="nozzleDiameter">
@@ -152,10 +147,8 @@
}
</select>
</label>
}
</div>
</div>
@if (mode() === "advanced") {
<div class="item-settings-grid">
<label>
{{ "CALC.PATTERN" | translate }}
@@ -179,7 +172,12 @@
<div class="item-settings-grid">
<label>
{{ "CALC.INFILL" | translate }}
<input type="number" min="0" max="100" formControlName="infillDensity" />
<input
type="number"
min="0"
max="100"
formControlName="infillDensity"
/>
</label>
<label class="item-settings-checkbox">
@@ -187,161 +185,72 @@
<span>{{ "CALC.SUPPORT" | translate }}</span>
</label>
</div>
}
</div>
} @else {
@if (getSelectedItem(); as selectedItem) {
<div class="item-settings-panel">
<h4 class="item-settings-title">
Impostazioni file: {{ selectedItem.file.name }}
</h4>
<div class="item-settings-grid">
<label>
{{ "CALC.MATERIAL" | translate }}
<select
[value]="selectedItem.material || form.get('material')?.value"
(change)="
updateItemMaterial(getSelectedItemIndex(), $any($event.target).value)
"
>
@for (mat of materials(); track mat.value) {
<option [value]="mat.value">{{ mat.label }}</option>
}
</select>
</label>
@if (mode() === "easy") {
<label>
{{ "CALC.QUALITY" | translate }}
<select
[value]="selectedItem.quality || form.get('quality')?.value"
(change)="
updateSelectedItemStringField(
'quality',
$any($event.target).value
)
"
>
@for (quality of qualities(); track quality.value) {
<option [value]="quality.value">{{ quality.label }}</option>
}
</select>
</label>
} @else {
<label>
{{ "CALC.NOZZLE" | translate }}
<select
[value]="
selectedItem.nozzleDiameter ?? form.get('nozzleDiameter')?.value
"
(change)="
updateSelectedItemNumberField(
'nozzleDiameter',
+$any($event.target).value
)
"
>
@for (n of nozzleDiameters(); track n.value) {
<option [value]="n.value">{{ n.label }}</option>
}
</select>
</label>
}
</div>
@if (mode() === "easy") {
<app-select
formControlName="quality"
[label]="'CALC.QUALITY' | translate"
[options]="qualities()"
></app-select>
} @else {
<app-select
formControlName="nozzleDiameter"
[label]="'CALC.NOZZLE' | translate"
[options]="nozzleDiameters()"
></app-select>
}
</div>
} @else {
@if (getSelectedItem(); as selectedItem) {
<div class="item-settings-panel">
<h4 class="item-settings-title">
Impostazioni file: {{ selectedItem.file.name }}
</h4>
@if (items().length > 1) {
<div class="checkbox-row sync-all-row">
<input type="checkbox" formControlName="syncAllItems" id="syncAllItems" />
<label for="syncAllItems">
Uguale per tutti i pezzi
</label>
</div>
}
<div class="item-settings-grid">
<label>
{{ "CALC.MATERIAL" | translate }}
<select formControlName="material">
@for (mat of materials(); track mat.value) {
<option [value]="mat.value">{{ mat.label }}</option>
}
</select>
</label>
@if (mode() === "advanced") {
<div class="item-settings-grid">
<label>
{{ "CALC.PATTERN" | translate }}
<select
[value]="selectedItem.infillPattern || form.get('infillPattern')?.value"
(change)="
updateSelectedItemStringField(
'infillPattern',
$any($event.target).value
)
"
>
@for (p of infillPatterns(); track p.value) {
<option [value]="p.value">{{ p.label }}</option>
}
</select>
</label>
<label>
{{ "CALC.NOZZLE" | translate }}
<select formControlName="nozzleDiameter">
@for (n of nozzleDiameters(); track n.value) {
<option [value]="n.value">{{ n.label }}</option>
}
</select>
</label>
</div>
<label>
{{ "CALC.LAYER_HEIGHT" | translate }}
<select
[value]="selectedItem.layerHeight ?? form.get('layerHeight')?.value"
(change)="
updateSelectedItemNumberField(
'layerHeight',
+$any($event.target).value
)
"
>
@for (l of layerHeights(); track l.value) {
<option [value]="l.value">{{ l.label }}</option>
}
</select>
</label>
</div>
<div class="item-settings-grid">
<label>
{{ "CALC.PATTERN" | translate }}
<select formControlName="infillPattern">
@for (p of infillPatterns(); track p.value) {
<option [value]="p.value">{{ p.label }}</option>
}
</select>
</label>
<div class="item-settings-grid">
<label>
{{ "CALC.INFILL" | translate }}
<input
type="number"
min="0"
max="100"
[value]="
selectedItem.infillDensity ?? form.get('infillDensity')?.value
"
(change)="
updateSelectedItemNumberField(
'infillDensity',
+$any($event.target).value
)
"
/>
</label>
<label>
{{ "CALC.LAYER_HEIGHT" | translate }}
<select formControlName="layerHeight">
@for (l of getLayerHeightOptionsForNozzle(form.get('nozzleDiameter')?.value); track l.value) {
<option [value]="l.value">{{ l.label }}</option>
}
</select>
</label>
</div>
<label class="item-settings-checkbox">
<input
type="checkbox"
[checked]="
selectedItem.supportEnabled ?? form.get('supportEnabled')?.value
"
(change)="updateSelectedItemSupport($any($event.target).checked)"
/>
<span>{{ "CALC.SUPPORT" | translate }}</span>
</label>
<div class="item-settings-grid">
<label>
{{ "CALC.INFILL" | translate }}
<input
type="number"
min="0"
max="100"
formControlName="infillDensity"
/>
</label>
<label class="item-settings-checkbox">
<input type="checkbox" formControlName="supportEnabled" />
<span>{{ "CALC.SUPPORT" | translate }}</span>
</label>
</div>
</div>
}
</div>
}
}
}
@@ -349,7 +258,6 @@
@if (items().length === 0 && form.get("itemsTouched")?.value) {
<div class="error-msg">{{ "CALC.ERR_FILE_REQUIRED" | translate }}</div>
}
</div>
<app-input
@@ -359,7 +267,6 @@
></app-input>
<div class="actions">
<!-- Progress Bar (Only when uploading i.e. progress < 100) -->
@if (loading() && uploadProgress() < 100) {
<div class="progress-container">
<div class="progress-bar">

View File

@@ -2,8 +2,8 @@
margin-bottom: var(--space-6);
}
.upload-privacy-note {
margin-top: var(--space-6);
margin-bottom: 0;
margin-top: var(--space-4);
margin-bottom: var(--space-1);
font-size: 0.8rem;
color: var(--color-text-muted);
text-align: left;
@@ -35,48 +35,50 @@
/* Grid Layout for Files */
.items-grid {
display: grid;
grid-template-columns: 1fr 1fr; /* Force 2 columns on mobile */
gap: var(--space-2); /* Tighten gap for mobile */
grid-template-columns: 1fr;
gap: var(--space-3);
margin-top: var(--space-4);
margin-bottom: var(--space-4);
@media (min-width: 640px) {
grid-template-columns: 1fr 1fr;
gap: var(--space-3);
}
}
.file-card {
padding: var(--space-2); /* Reduced from space-3 */
background: var(--color-neutral-100);
padding: var(--space-3);
background: var(--color-bg-card);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
transition: all 0.2s;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 4px; /* Reduced gap */
position: relative; /* For absolute positioning of remove btn */
min-width: 0; /* Allow flex item to shrink below content size if needed */
gap: var(--space-2);
position: relative;
min-width: 0;
&:hover {
border-color: var(--color-neutral-300);
box-shadow: 0 4px 10px rgba(10, 20, 30, 0.07);
}
&.active {
border-color: var(--color-brand);
background: rgba(250, 207, 10, 0.05);
background: rgba(250, 207, 10, 0.08);
box-shadow: 0 0 0 1px var(--color-brand);
}
}
.card-header {
overflow: hidden;
padding-right: 25px; /* Adjusted */
margin-bottom: 2px;
padding-right: 28px;
margin-bottom: 0;
}
.file-name {
font-weight: 500;
font-size: 0.8rem; /* Smaller font */
font-weight: 600;
font-size: 0.92rem;
color: var(--color-text);
display: block;
white-space: nowrap;
@@ -92,47 +94,46 @@
.card-controls {
display: flex;
align-items: flex-end; /* Align bottom of input and color circle */
gap: 16px; /* Space between Qty and Color */
align-items: flex-end;
gap: var(--space-4);
width: 100%;
}
.qty-group,
.color-group {
display: flex;
flex-direction: column; /* Stack label and input */
flex-direction: column;
align-items: flex-start;
gap: 0px;
gap: 2px;
label {
font-size: 0.6rem;
font-size: 0.72rem;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
letter-spacing: 0.3px;
font-weight: 600;
margin-bottom: 2px;
margin-bottom: 0;
}
}
.color-group {
align-items: flex-start; /* Align label left */
/* margin-right removed */
align-items: flex-start;
/* Override margin in selector for this context */
::ng-deep .color-selector-container {
margin-left: 0;
}
}
.qty-input {
width: 36px; /* Slightly smaller */
padding: 1px 2px;
width: 54px;
padding: 4px 6px;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
text-align: center;
font-size: 0.85rem;
font-size: 0.95rem;
font-weight: 600;
background: white;
height: 24px; /* Explicit height to match color circle somewhat */
height: 34px;
&:focus {
outline: none;
border-color: var(--color-brand);
@@ -141,10 +142,10 @@
.btn-remove {
position: absolute;
top: 4px;
right: 4px;
width: 18px;
height: 18px;
top: 6px;
right: 6px;
width: 20px;
height: 20px;
border-radius: 4px;
border: none;
background: transparent;
@@ -155,7 +156,7 @@
align-items: center;
justify-content: center;
transition: all 0.2s;
font-size: 0.8rem;
font-size: 0.9rem;
&:hover {
background: var(--color-danger-100);
@@ -170,7 +171,7 @@
.btn-add-more {
width: 100%;
padding: var(--space-3);
padding: 0.75rem var(--space-3);
background: var(--color-neutral-800);
color: white;
border: none;
@@ -193,6 +194,50 @@
}
}
.sync-settings {
margin-top: var(--space-4);
margin-bottom: var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-neutral-50);
padding: var(--space-3);
}
.sync-settings-toggle {
display: flex;
align-items: flex-start;
gap: var(--space-3);
cursor: pointer;
input[type="checkbox"] {
width: 20px;
height: 20px;
margin-top: 2px;
accent-color: var(--color-brand);
flex-shrink: 0;
}
}
.sync-settings-copy {
display: flex;
flex-direction: column;
gap: 2px;
}
.sync-settings-title {
font-size: 0.95rem;
font-weight: 700;
color: var(--color-text);
line-height: 1.2;
}
.sync-settings-subtitle {
font-size: 0.8rem;
font-weight: 500;
color: var(--color-text-muted);
line-height: 1.35;
}
.checkbox-row {
display: flex;
align-items: center;