From cecdfacd33e2a651008823a7ac503b3a4b69de87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Thu, 5 Feb 2026 17:13:56 +0100 Subject: [PATCH] feat(front-end): multiple file upload --- .../upload-form/upload-form.component.ts | 212 +++++++++++++----- 1 file changed, 154 insertions(+), 58 deletions(-) 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); } } }