diff --git a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts
index 1a92133..99eb7b5 100644
--- a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts
+++ b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts
@@ -1,4 +1,4 @@
-import { Component, input, output, signal } from '@angular/core';
+import { Component, input, output, signal, effect } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core';
@@ -9,6 +9,11 @@ import { AppButtonComponent } from '../../../../shared/components/app-button/app
import { StlViewerComponent } from '../../../../shared/components/stl-viewer/stl-viewer.component';
import { QuoteRequest } from '../../services/quote-estimator.service';
+interface FormItem {
+ file: File;
+ quantity: number;
+}
+
@Component({
selector: 'app-upload-form',
standalone: true,
@@ -20,28 +25,53 @@ import { QuoteRequest } from '../../services/quote-estimator.service';
@if (selectedFile()) {
-
- @for (f of files(); track f.name) {
-
- {{ f.name }}
-
- }
-
- } @else {
-
+
+
-
+
+
+
+ @if (items().length > 0) {
+
+ @for (item of items(); track item.file.name; let i = $index) {
+
+
+ {{ item.file.name }}
+ {{ (item.file.size / 1024 / 1024) | number:'1.1-2' }} MB
+
+
+
+
+ }
+
}
- @if (form.get('files')?.invalid && form.get('files')?.touched) {
+ @if (items().length === 0 && form.get('itemsTouched')?.value) {
{{ 'CALC.ERR_FILE_REQUIRED' | translate }}
}
@@ -59,12 +89,8 @@ import { QuoteRequest } from '../../services/quote-estimator.service';
[options]="qualities"
>
-
-
+
+
@if (mode() === 'advanced') {
@@ -113,7 +139,7 @@ import { QuoteRequest } from '../../services/quote-estimator.service';
{{ loading() ? (uploadProgress() < 100 ? 'Uploading...' : 'Processing...') : ('CALC.CALCULATE' | translate) }}
@@ -127,43 +153,95 @@ import { QuoteRequest } from '../../services/quote-estimator.service';
.error-msg { color: var(--color-danger-500); font-size: 0.875rem; margin-top: var(--space-2); text-align: center; }
.viewer-wrapper { position: relative; margin-bottom: var(--space-4); }
- .btn-clear {
+ .btn-clear-viewer {
position: absolute;
top: 10px;
right: 10px;
background: rgba(0,0,0,0.5);
color: white;
border: none;
- width: 32px;
- height: 32px;
- border-radius: 50%;
+ padding: 4px 8px;
+ border-radius: 4px;
cursor: pointer;
z-index: 10;
&:hover { background: rgba(0,0,0,0.7); }
}
- .file-list {
+ .items-list {
display: flex;
+ flex-direction: column;
gap: var(--space-2);
- overflow-x: auto;
- padding-bottom: var(--space-2);
+ margin-top: var(--space-4);
}
- .file-item {
- padding: 0.5rem 1rem;
+
+ .file-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: var(--space-3);
background: var(--color-neutral-100);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
- font-size: 0.85rem;
- cursor: pointer;
- white-space: nowrap;
- &:hover { background: var(--color-neutral-200); }
- &.active {
- border-color: var(--color-brand);
- background: rgba(250, 207, 10, 0.1);
- font-weight: 600;
+ transition: all 0.2s;
+
+ &.active {
+ border-color: var(--color-brand);
+ background: rgba(250, 207, 10, 0.05);
}
}
+ .file-info {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ cursor: pointer;
+ }
+ .file-name { font-weight: 500; font-size: 0.9rem; color: var(--color-text); }
+ .file-size { font-size: 0.75rem; color: var(--color-text-muted); }
+
+ .file-actions {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ }
+
+ .qty-control {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ label { font-size: 0.8rem; color: var(--color-text-muted); }
+ }
+
+ .qty-input {
+ width: 50px;
+ padding: 4px;
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-sm);
+ text-align: center;
+ font-size: 0.9rem;
+ &:focus { outline: none; border-color: var(--color-brand); }
+ }
+
+ .btn-remove {
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ border: none;
+ background: transparent; // var(--color-neutral-200);
+ color: var(--color-text-muted); // var(--color-danger-500);
+ font-weight: bold;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s;
+
+ &:hover {
+ background: var(--color-danger-100);
+ color: var(--color-danger-500);
+ }
+ }
+
.checkbox-row {
display: flex;
align-items: center;
@@ -185,9 +263,6 @@ import { QuoteRequest } from '../../services/quote-estimator.service';
/* Progress Bar */
.progress-container {
margin-bottom: var(--space-3);
- /* padding: var(--space-2); */
- /* background: var(--color-neutral-100); */
- /* border-radius: var(--radius-md); */
text-align: center;
width: 100%;
}
@@ -206,7 +281,6 @@ import { QuoteRequest } from '../../services/quote-estimator.service';
width: 0%;
transition: width 0.2s ease-out;
}
- .progress-text { font-size: 0.875rem; color: var(--color-text-muted); }
`]
})
export class UploadFormComponent {
@@ -217,7 +291,7 @@ export class UploadFormComponent {
form: FormGroup;
- files = signal
([]);
+ items = signal([]);
selectedFile = signal(null);
materials = [
@@ -252,10 +326,9 @@ export class UploadFormComponent {
constructor(private fb: FormBuilder) {
this.form = this.fb.group({
- files: [[], Validators.required],
+ itemsTouched: [false], // Hack to track touched state for custom items list
material: ['PLA', Validators.required],
quality: ['Standard', Validators.required],
- quantity: [1, [Validators.required, Validators.min(1)]],
notes: [''],
// Advanced fields
color: ['Black'],
@@ -267,14 +340,14 @@ export class UploadFormComponent {
onFilesDropped(newFiles: File[]) {
const MAX_SIZE = 200 * 1024 * 1024; // 200MB
- const validFiles: File[] = [];
+ const validItems: FormItem[] = [];
let hasError = false;
for (const file of newFiles) {
if (file.size > MAX_SIZE) {
hasError = true;
} else {
- validFiles.push(file);
+ validItems.push({ file, quantity: 1 });
}
}
@@ -282,32 +355,55 @@ export class UploadFormComponent {
alert("Alcuni file superano il limite di 200MB e non sono stati aggiunti.");
}
- if (validFiles.length > 0) {
- this.files.update(current => [...current, ...validFiles]);
- this.form.patchValue({ files: this.files() });
- this.form.get('files')?.markAsTouched();
- this.selectedFile.set(validFiles[validFiles.length - 1]);
+ if (validItems.length > 0) {
+ this.items.update(current => [...current, ...validItems]);
+ this.form.get('itemsTouched')?.setValue(true);
+ // Auto select last added
+ this.selectedFile.set(validItems[validItems.length - 1].file);
}
}
selectFile(file: File) {
- this.selectedFile.set(file);
+ if (this.selectedFile() === file) {
+ // toggle off? no, keep active
+ } else {
+ this.selectedFile.set(file);
+ }
}
- clearFiles() {
- this.files.set([]);
- this.selectedFile.set(null);
- this.form.patchValue({ files: [] });
+ updateItemQuantity(index: number, event: Event) {
+ const input = event.target as HTMLInputElement;
+ let val = parseInt(input.value, 10);
+ if (isNaN(val) || val < 1) val = 1;
+
+ this.items.update(current => {
+ const updated = [...current];
+ updated[index] = { ...updated[index], quantity: val };
+ return updated;
+ });
+ }
+
+ removeItem(index: number) {
+ this.items.update(current => {
+ const updated = [...current];
+ const removed = updated.splice(index, 1)[0];
+ if (this.selectedFile() === removed.file) {
+ this.selectedFile.set(null);
+ }
+ return updated;
+ });
}
onSubmit() {
- if (this.form.valid) {
+ if (this.form.valid && this.items().length > 0) {
this.submitRequest.emit({
+ items: this.items(), // Pass the items array
...this.form.value,
mode: this.mode()
});
} else {
this.form.markAllAsTouched();
+ this.form.get('itemsTouched')?.setValue(true);
}
}
}